关于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