zoukankan      html  css  js  c++  java
  • 关于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

  • 相关阅读:
    ORM之F和Q
    ORM查询
    Django
    jQuery基础
    DOM和BOM
    saas baas paas iaas 的理解
    分布式架构的演进过程
    tomcat 配置https 证书
    idea 学习总结
    简单数据库连接池-总结
  • 原文地址:https://www.cnblogs.com/Rex/p/1839537.html
Copyright © 2011-2022 走看看