编译器(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、代码存在并发的场景,有竞争的可能。
网友评论