要想理解Optimization barrier,先要理解Compiler Instruction Reorder,即编译器指令重排。
编译器指令重排是编译优化的结果,以gcc来说,它不知道为我们的代码默默做了多少事情,看看那整屏的优化选项就明了了。
本文以ubuntu下的gcc 4.4.3为实验,来逐步分析Optimization barrier的作用。
gcc的很多优化都可以造成指令重排,最常见的就是基本块重新排序(Basic block reordering)和指令调度(Instruction scheduling)。
为了解释Optimization barrier,我们只需要关注指令调度即可。
首先看指令调度的作用:
http://www.lingcc.com/gccint/RTL-passes.html#RTL-passes
上关于指令调度的解释(可能因为是中文翻译,不一定精确):
该过程寻找这样的指令,其输出在后来的指令中不会用到。在RISC机器上,内存加载和浮点指令经常会有这样的特征。它重新排序基本块中的指令以尝试将定义和使用分开,从而避免引起流水线阻塞。该过程执行两次,分别在寄存器分配之前和之后。该过程位于haifa-sched.c, sched-deps.c, sched-ebb.c, sched-rgn.c和sched-vis.c中。
实际编译选项中,这两个过程分别对应:-fschedule-insns和-fschedule-insns2
如果对目标机支持这个功能,它试图重新排列指令,以便消除因数据未绪造成的执行停顿.这可以帮助浮点运算或内存访问 较慢的机器调取指令,允许其他指令先执行,直到调取指令或浮点运算完成.
类似于`-fschedule-insns’选项,但是在寄存器分配完成后,需要一个额外的指令调度过程.对于 寄存器数目相对较少,而且取内存指令大于一个周期的机器,这个选项特别有用.
下面以一个例子说明(该例子不知道摘自哪篇关于内存屏障的论文中):
1
2
3
4
5
6
7
|
volatile
int
ready;
int
message[100];
void
no_cmb (
int
i) {
message[i/10] = 42;
ready = 1;
}
|
首先,不优化之,即gcc -S cmb.c -o cmb_no_opt.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.globl no_cmb
.type no_cmb, @function
no_cmb:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx
movl $1717986919, %edx
movl %ecx, %eax
imull %edx
sarl $2, %edx
movl %ecx, %eax
sarl $31, %eax
movl %edx, %ecx
subl %eax, %ecx
movl %ecx, %eax
movl $42, message(,%eax,4)
movl $1, ready
popl %ebp
ret
|
可以看到,不优化的no_cmb函数中,是保持的原有的代码序的.
至于为什么i/10的汇编代码为什么是这个样子,可以参照别人的一篇博客http://blog.csdn.net/mathe/article/details/1153575
分别以-fschedule-insns和-fschedule-insns2优化之,如下:
1) gcc -S -fschedule-insns cmb.c -o cmb.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.globl no_cmb
.type no_cmb, @function
no_cmb:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx
movl $1717986919, %edx
movl $1, ready
movl %ecx, %eax
imull %edx
movl %ecx, %eax
sarl $31, %eax
sarl $2, %edx
movl %edx, %ecx
subl %eax, %ecx
movl %ecx, %eax
movl $42, message(,%eax,4)
popl %ebp
ret
|
2) gcc -S -fschedule-insns2 cmb.c -o cmb2.s
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.globl no_cmb
.type no_cmb, @function
no_cmb:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx
movl $1717986919, %edx
movl %ecx, %eax
imull %edx
sarl $2, %edx
movl %ecx, %eax
sarl $31, %eax
movl %edx, %ecx
subl %eax, %ecx
movl %ecx, %eax
movl $42, message(,%eax,4)
movl $1, ready
popl %ebp
ret
|
可以看到,-fschedule-insns2对no_cmb函数看不到效果,而-fschedule-insns使no_cmb代码发生的重排,ready = 1被排到message[i/10] = 42之前了。
为什么-fschedule-insns会有这样的效果呢?我们再来回顾一下它的功能:
它试图重新排列指令,以便消除因数据未绪造成的执行停顿
no_cmb函数中只包含两条语句:
1
2
|
message[i/10] = 42;
ready = 1;
|
其中message[i/10]相关的指令比较多,而ready相关的指令只有一条,我们知道,CPU处理指令都是
流水进行的,无依赖不冲突的指令可以并行处理,因为对应的执行单元是空闲的,而message[i/10]相关的指令都是有依赖的,不能够乱序执行,故将ready=1排在前面,优化CPU的流水处理。如果不这么做,可能CPU就将浪费数个指令周期来完成ready=1的操作了。这么看来,这个优化还是做对了。
回过头来看看ready变量的类型,不错,的确是volatile的。
volatile这个玩意儿的确很容易让人误解,以下几种场景就是大家对volatile理解的缩影:
1)volatile修饰的变量,每次读写都直接访存;
2)volatile像是gcc优化的局部开关,对于volatile修饰的变量,不对其进行优化;
两种理解都不是完全正确的,不过大体方向上的指引也没什么偏斜,知识的表达往往就是这样:能够指引正确方位的知识就是有作用的,更加精确的解释,可能需要花费更多的精力和篇章,结果更为复杂,学习它的人反而更难以接受,所以,更加精确的解释,需要学习它的人自己去领悟和摸索。
有点扯多了,我们来看volatile的第二条理解,既然volatile修饰的变量是不做优化的,那为什么还会将ready=1排到前面去呢?其实可以这样来理解:不是将ready=1排到前面,而是将message[i/10]较复杂的指令序列排到后面,这样就没有违反这个概念了。
就因为volatile这么容易让人误解,对代码重排也做不到足够的控制,非常多的人宣扬volatile在多线程编程中无用论。
现在问题就来了,原本ready变量采用volatile变量,意图很明显,是想保证ready=1和message[i/10]=42对应指令的有序性,结果,代码优化后,结果却违反了意图。
那么,为了达到这个目标,gcc总应该提供一种方法吧。
面包会有的,方法也会有的:
gcc提供了内联汇编的语句可以做到这一点:
1
2
3
|
asm
volatile
(
""
:::
"memory"
);
// or
__asm__ __volatile__ (
""
:::
"memory"
);
|
这即是本文主要想说明的Optimization barrier.
先看其能否解决ready的问题:
先在两句中插入Optimization barrier,则代码如下:
1
2
3
4
5
6
7
8
|
volatile
int
ready;
int
message[100];
void
cmb (
int
i) {
message[i/10] = 42;
__asm__ __volatile__ (
""
:::
"memory"
);
ready = 1;
}
|
同样以gcc -S -fschedule-insns cmb.c -o cmb.s编译之:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.globl cmb
.type cmb, @function
cmb:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %ecx
movl $1717986919, %edx
movl %ecx, %eax
imull %edx
movl %ecx, %eax
sarl $31, %eax
sarl $2, %edx
movl %edx, %ecx
subl %eax, %ecx
movl %ecx, %eax
movl $42, message(,%eax,4)
movl $1, ready
popl %ebp
ret
|
OK,发现顺序正确了,而且居然没有新增指令,这是怎么回事呢?__asm__ __volatile__ (“” ::: “memory”)跑哪里去了。