美文网首页
编译器乱序 和 显示编译器屏障

编译器乱序 和 显示编译器屏障

作者: wayyyy | 来源:发表于2021-02-28 03:32 被阅读0次

    编译器(compiler)的工作之一是优化我们的代码以提高性能,这包括在不改变程序行为的情况下重新排列指令。

    compiler 在无锁编程的时候不知道代码需要线程安全(thread-safe),所以compiler假设我们的代码都是单线程执行(single-threaded),并且进行指令重排优化并保证是单线程安全的。

    因此,当你不需要compiler重新排序指令的时候,你需要显式告编译器,我不需要重排,否则,它可不会听你的。

    例子一
    int run = 1;
    
    void foo()
    {
        while (run)
            ;
    }
    

    run是个全局变量,foo()在一个进程中执行,一直循环。我们期望的结果是foo()一直等到其他进程修改run的值为 0 才退出循环。当然这时候,你会想到run应该用互斥锁来保护,但假设这里我们需要做的是无锁编程。

    实际compiler编译的代码和我们会达到我们预期的结果吗?我们看一下汇编代码。

    int main()                                                                           
    {                                                                                    
        while(run)                                                                       
     4f0:   8b 05 1a 0b 20 00       mov    0x200b1a(%rip),%eax        # 201010 <run>     
     4f6:   66 2e 0f 1f 84 00 00    nopw   %cs:0x0(%rax,%rax,1)                          
     4fd:   00 00 00                                                                     
     500:   85 c0                   test   %eax,%eax                                     
     502:   75 fc                   jne    500 <main+0x10>                               
            ;                                                                            
    }                                                                                    
    

    compiler首先将run加载到一个寄存器 reg 中,然后判断 reg 是否满足循环条件,如果满足就一直循环。但是循环过程中,寄存器reg的值并没有变化。因此,即使其他进程修改run的值为 0,也不能使 foo() 退出循环。很明显,这不是我们想要的结果。我们继续看一下加入barrier()后的结果。

    如果显示加上编译器屏障:

    #define barrier() __asm__ __volatile__("": : :"memory")
    
    void foo()
    {
        while (run)
            barrier();
    }
    
    00000000000004f0 <main>:
    #define barrier() __asm__ __volatile__("": : :"memory")
    
    int run = 1;
    
    int main()
    {
    ...
        while(run)
     4f8:   8b 05 12 0b 20 00       mov    0x200b12(%rip),%eax        # 201010 <run>
     4fe:   85 c0                   test   %eax,%eax
     500:   75 f6                   jne    4f8 <main+0x8>
            barrier();
    }
    ...
    

    我们可以看到加入barrier()后的结果真是我们想要的。每一次循环都会从内存中重新load run的值。因此,当有其他进程修改run的值为0的时候,foo()可以正常退出循环。

    为什么加入barrier()后的汇编代码就是正确的呢?
    因为barrier()作用是告诉 compiler 内存中的值已经变化,后面的操作都需要重新从内存load,而不能使用寄存器缓存的值。

    例子二
    CPU乱序 和 编译器乱序的关系

    smp_wmb smp_rmb smp_mb等都是防止CPU乱序的指令封装,是不是意味着这些接口仅仅阻止CPU乱序,而允许编译器乱序呢?
    答案肯定是不可能的,这里有个点需要记住,所有的CPU内存屏障封装都隐士包含了编译器屏障。

    什么时候需要考虑编译器乱序的问题

    1、有共享数据需要访问,而且是无锁访问。
    2、代码存在并发的场景,有竞争的可能。

    相关文章

      网友评论

          本文标题:编译器乱序 和 显示编译器屏障

          本文链接:https://www.haomeiwen.com/subject/rlbrtltx.html