并发程序的乱序之一:编译器指令重排
来源:互联网 发布:大大网络用语什么意思 编辑:程序博客网 时间:2024/06/05 01:13
一、编译器想做什么
编译器的优化,希望将整个函数用最少的时钟周期来实现。
对于编译器看到的,没有直接关系的不同变量(无volatile),可以进行乱序的指令调度,而对于相同变量或者有别名或者传播关系的变量,需要按照编译器静态分析的依赖分析结果进行合理调度[注1]。
假设有如下场景:假设该架构下,读取指令从发出到实际读取到数据需要等待2个时钟周期,计算c = b * 3需要一个时钟周期。
{ load a; load b; c = b * 3; use a and c;}正常执行的顺序如下:
{ load instruction for a (cycle 0); load instruction for b (cycle 1); wait for b's loading (cycle 2); wait for b's loading (cycle 3); calculate for c using b (cycle 4); use a and c (cycle 5);}
打乱执行顺序之后:
{ load instruction for b (cycle 0); load instruction for a (cycle 1); --> padding (wait for b's loading (cycle 1);) wait for b's loading (cycle 2); calculate for c using b (cycle 3); use a and c (cycle 4);}可以看出,打乱执行顺序之后,节约了一个时钟周期。
二、对并发的影响:
假设并发时的一种使用场景:假设读写都只有一个线程,写线程中,数据写完时设置标志位。读线程中,通过对flag的判断来对数据进行使用。
编译器进行处理时,data和flag并没有直接的关系(有用户自己指定的隐性关系,但是编译器并不知道),如果编译器进行非常激进的优化,在写线程中,先设置了flag,再写数据,或者在读线程中,将data作为一个不变量,提早进行读取,获取到的data值都是不正确的。
因此,编译器进行的指令重排,会破坏用户代码中存在隐性关系的变量之间的控制流[2]。
write_thread:{ write data; set flag;}read_thread:{ if (flag) { use data; }}
可能的顺序:
write_thread{ set flag; write data;}read_thread{ tmp = data; if (flag) { use tmp; }}
三、volatile关键字:
volatile关键字会告诉编译器,这是一个易变(不变const,可变mutable)的变量,保证编译器每次对其使用时需要重新载入。同时指示编译器不要对该变量进行激进的优化,对性能会有较大的影响。
volatile场景1:重新载入
#include <assert.h>#include <thread>using namespace std;int flag;void write (){ flag = 1;}void read (){ while (!flag);}int main (){ flag = 0; thread t1 (write); thread t2 (read); t1.join (); t2.join (); return 0;}当开启编译器优化(O1以上),程序将无法获取结果,原因在于read中,开启优化以后,一直访问的是寄存器中保存的flag值,当第一次访问flag且值为0,就进入了L5的无限循环。这种场景,flag应该使用volatile,告诉编译器这是一个易变的值,每次对于flag的访问应该使用内存中的值,而不是使用寄存器中保存的临时值。
.cfi_startproc movl flag(%rip), %eax testl %eax, %eax je .L5 rep; ret.L5: jmp .L5 .cfi_endproc使用volatile后的汇编:当flag为0时,会重新进入循环,循环中重新获取了flag的值。伴随着多次的load(load的流水一般简单计算指令的要长),会导致性能急剧下降,如果不是必须,尽量不要误用。
.cfi_startproc .p2align 4,,10 .p2align 3.L4: movl flag(%rip), %eax testl %eax, %eax je .L4 rep; ret .cfi_endproc
volatile场景2:阻止优化(来自[3]的用例5、6):
int a, b;void foo (){ a = b + 1; b = 0;}两个变量都没加volatile时,汇编如下:先读取b的内容,在a还没有计算出结果之前就提前修改了b的值,然后根据第一行读出来的b的值去计算a。
当多线程执行时,如果上下文切换发生在修改b和addl之间,这时候其他线程读取到的就是一个已经置位的b,且尚未计算完成的a。
.cfi_startproc movl b(%rip), %eax //读取b movl $0, b(%rip) //修改b addl $1, %eax movl %eax, a(%rip) ret .cfi_endproc当加上volatile关键字时,汇编如下:此时b的置零发生在a的值计算完成且写回之后,a与b之间的顺序与源码一致。
.cfi_startproc movl b(%rip), %eax addl $1, %eax movl %eax, a(%rip) movl $0, b(%rip) ret .cfi_endproc
注1:
相同变量之间存在三种依赖关系[1]:
a = 1; b = a; //先写后读,真依赖b = a; a = 1; //先读后写,反依赖a = 1; a = 2; //写后写,输出依赖
参考资料:
[1] 编译原理 P452
[2] http://blog.csdn.net/beiyetengqing/article/details/49580559
[3] http://hedengcheng.com/?p=725
- 并发程序的乱序之一:编译器指令重排
- 奇怪的并发现象探究——JMM的指令重排、内存级指令重排
- 指令重排的基本原则
- java虚拟机的指令重排和CPU的指令重排
- SSE特殊指令集系列之一----各种数据重排指令
- 处理器的乱序和并发执行
- 处理器的乱序和并发执行
- 处理器的乱序和并发执行
- 理解并发编程中的重要概念:指令重排序和指令乱序执行
- Java并发:volatile内存可见性和指令重排
- Java并发:volatile内存可见性和指令重排
- Java并发:volatile内存可见性和指令重排
- Java并发:volatile内存可见性和指令重排
- Java并发:volatile内存可见性和指令重排
- Java并发:volatile内存可见性和指令重排
- Java并发:volatile内存可见性和指令重排
- Java并发:volatile内存可见性和指令重排
- Volatile的可見性與禁止指令重排
- xcode插件
- Writing a Linux Keylogger in C
- 自定义ViewGroup--CascadeLayout
- vba脚本excel动态创建索引目录表
- 第1章第2节练习题3 删除最小值结点
- 并发程序的乱序之一:编译器指令重排
- DFS基础题-----HDOJ1241
- Validate (Check) dimensions (Height and Width) of Image before Upload using HTML5, JavaScript and jQ
- python + eric 安装
- Shiro权限验证标签
- Qt5.5.0 vs2013 64位编译
- ZOJ 2095 Divisor Summation (求因子和)
- mysql 多条件联表
- iOS多线程(GCD重点介绍)