• 放弃所谓“右移优化除法”行为


    The Difference between Division and Arithmetic Right Shifting in C

    你是否有听说过有符号数不能使用右移操作(>>)来代替除法? 这篇短文会向你证明它,并尝试向你解释为什么。当然,如果你没有听说过,那么从现在开始,记住它!

    Foundation: Logical Shift .vs. Arithmetic Shift

    若你现在有二进制数x=1110B,对其施加右移操作,请问高位填0还是填1?

    逻辑移位不管造成的影响,总是用0来填充移位操作产生的空缺。但是这样简单的想法在一些情况总会出错。例如若上述x是有符号数,那么简单的填0就会造成错误,起码正负号出错了。

    算数移位支持有符号数的移位操作,在移位后使用符号位进行填充,结合补码的表示方法,就能实现正确的负数移位操作。

    总结来说:在有符号的场景下,使用算数位移;如果你能保证移位操作是无符号的,那么用逻辑位移也无妨.

    x86汇编代码中,shr代表逻辑右移指令,sar代表算数右移指令,我们可以通过以下C代码及其反汇编的结果来更好的理解逻辑移位和算数移位:

    #include <stdlib.h>
    #include <stdio.h>
        
    signed int x = -3;
    unsigned int y = 3;
    
    int main()
    {
        x >>= 1;
        y >>= 1;
        return 0;
    }
    
    
    x:
            .long   -3
    y:
            .long   3
    main:
            push    rbp
            mov     rbp, rsp
            mov     eax, DWORD PTR x[rip]
            sar     eax
            mov     DWORD PTR x[rip], eax
            mov     eax, DWORD PTR y[rip]
            shr     eax
            mov     DWORD PTR y[rip], eax
            mov     eax, 0
            pop     rbp
            ret
    

    https://godbolt.org/z/K4M4Ko4c7

    Demo to Prove the Subject

    在我作为一个初级程序员的认知中,/2>>1是等价的,甚至一起还听说过后者能够优化代码的效率。但是今天我要告诉你, Definitely wrong!

    或许在遥远的古代,我们使用位移操作真的能够对代码进行加速,但是当下编译器已经足够聪明,如果你真的动手反汇编"/2"的代码,那么你就会知道编译器已经替你优化为了位移操作。

    更糟糕的是,我们要避免使用移位操作来实现除法或者乘法,不仅仅是因为这两者等价,实际上,他们并不是等价的!并且会造成错误!

    Let me show you a little demo.

    考虑如下的C语言代码:

    #include <stdlib.h>
    #include <stdio.h>
    
    signed int x = -3;
    signed int y = -3;
    
    int main()
    {
        x >>= 1;
        y /= 2;
        return 0;
    }
    
    

    他们的汇编代码是相同的吗?这里还是拿X86汇编举例:

    ; Following is ‘x >>= 1’
    mov     eax, #-3  ;x
    sar     eax
    mov     x, eax
    ; Following is ‘y/= 2’
    mov     eax, #-3  ;y
    mov     edx, eax
    shr     edx, 31
    add     eax, edx
    sar     eax
    mov     y, eax
    

    注意:以上的汇编代码省去了一些我认为无关紧要的操作,并不是完全正确的,但是足够表达他们的差别了。

    可以看出,除法比移位多了一步shr edx, 31过程,下面会探讨这个。

    还有一件使你震惊的事件,x, y的值最终是不同的!是的,正是因为那条看似“多余”的shr指令。

    Why does This happen?

    首先,我们可以确定的一件事是:编译器真的帮我们将除法操作优化为移位。所以,再也不要说你的代码中使用>>来替代除法是为了增加执行效率了。

    让我们来解释下为什么两者的结果是不同的。

    首先,sar指令在x86指令集中表示算数右移,这个是我们熟悉的,那么-3进行算数右移后的结果就是-2. 意味着>>是向负无穷舍入的.

    那么除法操作又是在干什么呢? 它是将原值加上其符号位.Demo中使用的数据类型是32位int.

    shr     edx, 31
    add     eax, edx
    

    这样做必然改变了原值啊,动手算一下就会知道,-3/2的结果为-1. 并且只有负奇数会受影响,对于正数,其符号为0;对于负偶数,其补码的最低位必为0,刚加上的1会被下一步的算数右移丢弃,不对高位产生影响。

    Aha, 差别就是向负无穷舍弃还是向0舍弃,一时间竟然不知道哪个是正确的了。

    What Should We Do?

    根据最新的[C语言标准草案](ISO/IEC 9899:201x (open-std.org)) 6.5.7章节,负数的右移操作是implementation-defined,即取决于具体的实现:

    The result of E1 >> E2 is E1 right-shifted E2 bit positions. If E1 has an unsigned type or if E1 has a signed type and a nonnegative value, the value of the result is the integral part of the quotient of E1 / 2E2. If E1 has a signed type and a negative value, the resulting value is implementation-defined.

    因此,理论上它依赖于实现。所以我们在实际应用中为了程序的可移植性,应当避免对有符号数使用移位操作。除非你能确定它的值一定是非负数,在此情况下,请将它用无符号类型来声明。

    对于除法操作,标准中的6.5.5章节规定了,除法操作总是向0舍入. 非常好!

    When integers are divided, the result of the / operator is the algebraic quotient with any fractional part discarded.

    检查你的代码,恢复所有的“优化”乘除法的行为吧!

  • 相关阅读:
    js eval函数写一个简单的计算器
    nginx方向代理
    nodejs环境的搭建(linux环境centos6.5)
    vue渲染数据后与owlCarousel轮播插件冲突,失效
    vue 配合vue-resource调用接口,获取数据
    高效生成随机数并去重
    nginx 调整配置文件支持TP框架
    noVNC 搭建
    python基础学习-socket1 初识socket
    python基础学习-面向对象-类的使用
  • 原文地址:https://www.cnblogs.com/bluettt/p/16186598.html
Copyright © 2020-2023  润新知