关于x86下VB、C#、VC中的整数运算需要注意的地方

关于x86下VB、C#、VC中的整数运算需要注意的地方

 

请大家看这段代码:

 

using System;

namespace IntegerArithmetic
{
    class Program
    {
        static void Main(string[] args)
        {
            Int32 a = (-1) / 8;                   //0
            Int32 b = (-1) % 8;                   //-1
            Int32 c = 1 << 32;                    //1
            UInt32 d = 1U << 32;                  //1
  
            Int32 e = (-1) / (-8);                //0
            Int32 f = (-1) % (-8);                //-1
            Int32 g = (-1) << 32;                 //-1
            Int32 h;                              //-1
            Int32 i = Math.DivRem(-1, 8, out h);  //0

            Console.WriteLine(String.Format("a = {0}", a));
            Console.WriteLine(String.Format("b = {0}", b));
            Console.WriteLine(String.Format("c = {0}", c));
            Console.WriteLine(String.Format("d = {0}", d));
            Console.WriteLine(String.Format("e = {0}", e));
            Console.WriteLine(String.Format("f = {0}", f));
            Console.WriteLine(String.Format("g = {0}", g));
            Console.WriteLine(String.Format("h = {0}", h));
            Console.WriteLine(String.Format("i = {0}", i));
        }
    }
}

 

这些结果中不少是反常识的。


1.整数除法和模运算

在x86下的VB、C#、VC中,整数除法和模运算的定义为

x DIV y = TruncToZero(x / y)
x MOD y = x - (x DIV y) * y

其中
MOD表示模运算(VB中为Mod, C#和C++中为%);
DIV表示整数除法(VB中为\,C#和C++中为/);
TruncToZero(r)是指取一个符号与r相同,且绝对值不大于|r|的绝对值最大的整数;
TruncToZero(r) = sign(r) * max{|x|: |x| <= |r|}
/表示实数除法。

这种定义导致的问题是(负数 MOD 正数)的结果为负数。
例如[1]:

bool is_odd(int n) {
    return n % 2 == 1;
}

这个函数在传入任意负数n时会返回false。

 

这几种语言在x86下的表现,可能是编译器考虑到运行效率直接使用x86机器指令IDIV实现的缘故。


有两种修正的定义,请参阅[1]:

floored division:模得的值的符号与模数一致

x DIV y = floor(x / y)
x MOD y = x - (x DIV y) * y

Euclidean definition: 模得的值始终为正

x DIV y = if y > 0
              floor(x / y)
          else
              ceil(x / y)
x MOD y = x - (x DIV y) * y


我们可以在C#中实现采用floored division方式修正的代码。

 

/// <summary>modulo from Knuth's floored division</summary>
public static Int32 Mod(this Int32 a, Int32 m) {
    Int32 s = Math.Sign(m);
    Int32 pm = Math.Abs(m);
    return s * (((s * a) % pm) + pm) % pm;
}

/// <summary>Knuth's floored division</summary>
public static Int32 Div(this Int32 a, Int32 b)
{
    return (a - a.Mod(b)) / b;
}

 

不过这个方法可能会出现整数溢出。特别是C#默认没有开启整数溢出异常,可能导致计算出错。
下面是没有整数溢出的版本。不过正确是有代价的,逻辑很复杂。 

public static Int32 Mod(this Int32 a, Int32 m)
{
    Int32 r = a % m;
    if (((r < 0) && (m > 0)) || ((r > 0) && (m < 0))) { r += m; }
    return r;
}

public static Int32 Div(this Int32 a, Int32 b)
{
    if (b == 0) { throw new DivideByZeroException(); }
    Int32 r = a.Mod(b);
    if ((a > 0) && (r < 0))
    {
        if (a - Int32.MaxValue > r) { return (a - Math.Abs(b) - r) / b + Math.Sign(b); }
    }
    else if ((a < 0) && (r > 0))
    {
        if (a - Int32.MinValue < r) { return (a + Math.Abs(b) - r) / b - Math.Sign(b); }
    }
    return (a - r) / b;
}


2.移位运算
在x86下的VB、C#、VC中,移位运算的定义为

 

Int32 x, Int32 y
x << y = x SAL (y MOD 32)
x >> y = x SAR (y MOD 32)
UInt32 x, Int32 y
x << y = x SHL (y MOD 32)
x >> y = x SHR (y MOD 32)

其中SAR是最高位补原最高位的算术右移,SHR是最高位补0的逻辑右移,SAL、SHL是左移。
y MOD 32 = y AND 0x1F

这应该是x86指令集所决定的。

不过需要注意到VC编译器对常数和变量的处理不一致。
在y为常数且超过0..31的范围时,会出现“shift count negative or too big, undefined behavior”的警告。
当x也为常数时,常量会按常识正确计算。

修正:

public static UInt32 SHL(this UInt32 a, Int32 n)
{
    if (n >= 32) { return 0; }
    if (n < 0) { return a.SHR(-n); }
    return a << n;
}

public static UInt32 SHR(this UInt32 a, Int32 n)
{
    if (n >= 32) { return 0; }
    if (n < 0) { return a.SHL(-n); }
    return a >> n;
}

public static Int32 SAL(this Int32 a, Int32 n)
{
    if (n >= 32) { return 0; }
    if (n < 0) { return a.SAR(-n); }
    return a << n;
}

public static Int32 SAR(this Int32 a, Int32 n)
{
    if (n >= 32)
    {
        if (Convert.ToBoolean(a & Int32.MinValue))
        {
            return -1;
        }
        else
        {
            return 0;
        }
    }
    if (n < 0) { return a.SAL(-n); }
    return a >> n;
}

 


3.修正的使用时机

前述的两个修正是完备的。但是不能很好的融入语法,且性能损失是可以预测到的。
因此,下面给出使用的时机判断方法。

1)整数除法和模运算修正的使用时机是:
被除数x和除数y中有一个可能为负数的时候。

通常除数是正数,而被除数有时候是负数。
但是,有时被除数看起来可能会出现负数,却可以较容易的修正为正数表达式,如:

(n - 1) MOD m

其中n为非负整数,m为正整数。
这里n = 0时不修正会出现问题。
但是我们可以写成

(n + m -1) MOD m

这个就不会出现问题。

 

2)移位运算修正的使用时机

在移位的位数y为变量时使用。
例如我们需要获得一个掩码。

Int32 Mask = 1 << n - 1

这里n为Int32变量。
则我们必须使用

Int32 Mask = 1.SAL(n) - 1

否则,在n = 32时会出现问题。


4.结论

x86下的整数运算远比人们所想象的复杂。
稍不注意,就会导致出现无法察觉的bug。


参考:
[1] http://en.wikipedia.org/wiki/Modulo_operation

 

posted @ 2010-09-30 15:34 地狱门神 阅读(...) 评论(...) 编辑 收藏