关于x86下VB、C#、VC中的整数运算需要注意的地方
关于x86下VB、C#、VC中的整数运算需要注意的地方
请大家看这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | 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中,整数除法和模运算的定义为
1 2 | 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]:
1 2 3 | bool is_odd( int n) { return n % 2 == 1; } |
这个函数在传入任意负数n时会返回false。
这几种语言在x86下的表现,可能是编译器考虑到运行效率直接使用x86机器指令IDIV实现的缘故。
有两种修正的定义,请参阅[1]:
floored division:模得的值的符号与模数一致
1 2 | x DIV y = floor(x / y) x MOD y = x - (x DIV y) * y |
Euclidean definition: 模得的值始终为正
1 2 3 4 5 | x DIV y = if y > 0 floor(x / y) else ceil(x / y) x MOD y = x - (x DIV y) * y |
我们可以在C#中实现采用floored division方式修正的代码。
1 2 3 4 5 6 7 8 9 10 11 12 | /// <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#默认没有开启整数溢出异常,可能导致计算出错。
下面是没有整数溢出的版本。不过正确是有代价的,逻辑很复杂。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | 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中,移位运算的定义为
1 2 3 4 5 6 | 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也为常数时,常量会按常识正确计算。
修正:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | 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中有一个可能为负数的时候。
通常除数是正数,而被除数有时候是负数。
但是,有时被除数看起来可能会出现负数,却可以较容易的修正为正数表达式,如:
求
1 | (n - 1) MOD m |
其中n为非负整数,m为正整数。
这里n = 0时不修正会出现问题。
但是我们可以写成
1 | (n + m -1) MOD m |
这个就不会出现问题。
2)移位运算修正的使用时机
在移位的位数y为变量时使用。
例如我们需要获得一个掩码。
1 | Int32 Mask = 1 << n - 1 |
这里n为Int32变量。
则我们必须使用
1 | Int32 Mask = 1.SAL(n) - 1 |
否则,在n = 32时会出现问题。
4.结论
x86下的整数运算远比人们所想象的复杂。
稍不注意,就会导致出现无法察觉的bug。
参考:
[1] http://en.wikipedia.org/wiki/Modulo_operation
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· .NET周刊【3月第1期 2025-03-02】
· 分享 3 个 .NET 开源的文件压缩处理库,助力快速实现文件压缩解压功能!
· [AI/GPT/综述] AI Agent的设计模式综述