優化屏障(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”)跑哪裏去了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章