优化屏障(Optimization barrier)第一讲

1. 编译优化导致编译器指令重排

要想理解Optimization barrier,先要理解Compiler Instruction Reorder,即编译器指令重排。
编译器指令重排是编译优化的结果,以gcc来说,它不知道为我们的代码默默做了多少事情,看看那整屏的优化选项就明了了。
本文以ubuntu下的gcc 4.4.3为实验,来逐步分析Optimization barrier的作用。
gcc的很多优化都可以造成指令重排,最常见的就是基本块重新排序(Basic block reordering)和指令调度(Instruction scheduling)。
为了解释Optimization barrier,我们只需要关注指令调度即可。

1.1 指令调度

首先看指令调度的作用:
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

1)-fschedule-insns

如果对目标机支持这个功能,它试图重新排列指令,以便消除因数据未绪造成的执行停顿.这可以帮助浮点运算或内存访问 较慢的机器调取指令,允许其他指令先执行,直到调取指令或浮点运算完成.

2)-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的操作了。这么看来,这个优化还是做对了。

2. 为了防止编译器指令重排

回过头来看看ready变量的类型,不错,的确是volatile的。

2.1 volatile让人费解的语义

volatile这个玩意儿的确很容易让人误解,以下几种场景就是大家对volatile理解的缩影:
1)volatile修饰的变量,每次读写都直接访存;
2)volatile像是gcc优化的局部开关,对于volatile修饰的变量,不对其进行优化;

两种理解都不是完全正确的,不过大体方向上的指引也没什么偏斜,知识的表达往往就是这样:能够指引正确方位的知识就是有作用的,更加精确的解释,可能需要花费更多的精力和篇章,结果更为复杂,学习它的人反而更难以接受,所以,更加精确的解释,需要学习它的人自己去领悟和摸索。
有点扯多了,我们来看volatile的第二条理解,既然volatile修饰的变量是不做优化的,那为什么还会将ready=1排到前面去呢?其实可以这样来理解:不是将ready=1排到前面,而是将message[i/10]较复杂的指令序列排到后面,这样就没有违反这个概念了。

就因为volatile这么容易让人误解,对代码重排也做不到足够的控制,非常多的人宣扬volatile在多线程编程中无用论。

2.2 采用编译器内存屏障来解决代码重排的问题

现在问题就来了,原本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”)跑哪里去了。

你可能感兴趣的:(优化屏障(Optimization barrier)第一讲)