關於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 yx << y = x SAL (y MOD 32)x >> y = x SAR (y MOD 32)UInt32 x, Int32 yx << 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