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

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

作者: 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