编译器的优化,希望将整个函数用最少的时钟周期来实现。
对于编译器看到的,没有直接关系的不同变量(无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关键字会告诉编译器,这是一个易变(不变const,可变mutable)的变量,保证编译器每次对其使用时需要重新载入。同时指示编译器不要对该变量进行激进的优化,对性能会有较大的影响。
#include
#include
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
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]:
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