1. gcc編譯的大致過程
可以看到,gcc優化主要分兩大部分:Tree優化和RTL(Register Transfer Language)優化;
前文所說的指令調度(Instruction scheduling)即爲RTL優化的一部分。
2. 從RTL指令調度出發,追尋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可以-dr輸出過程中的rtl文件,現在這個版本-dr好像不認識,所以只好先用-da輸出全部信息,即gcc -da basic_cmb.c
執行後發現生成一大堆basic_cmb.c.XXXr.YYY文件,我們找到basic_cmb.c.128r.expand即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
|
;; ;;
Full RTL generated for
this
function: //
;; 表註釋 ;; (note
1 0 3 NOTE_INSN_DELETED) //
調試和說明信息,不用關心 ;;
Start of basic block ( 0) -> 2 ;;
Pred edge ENTRY (fallthru) (note
3 1 2 2 [bb 2] NOTE_INSN_BASIC_BLOCK) //
調試和說明信息,不用關心 (note
2 3 4 2 NOTE_INSN_FUNCTION_BEG) //
調試和說明信息,不用關心 ;;
End of basic block 2 -> ( 3) ;;
Succ edge 3 [100.0%] (fallthru) ;;
Start of basic block ( 2) -> 3 ;;
Pred edge 2 [100.0%] (fallthru) (note
4 2 5 3 [bb 3] NOTE_INSN_BASIC_BLOCK) //
調試和說明信息,不用關心 (insn
5 4 6 3 basic_cmb.c:5 (set (reg:SI 59) //
virtual-incoming-args表示入參,即reg59=入參 (mem/c/i:SI
(reg/f:SI 53 virtual -incoming-args)
[0 i+0 S4 A32])) -1 (nil)) (insn
6 5 7 3 basic_cmb.c:5 (set (reg:SI 61) //
reg61=1717986919 (const_int
1717986919 [0x66666667])) -1 (nil)) (insn
7 6 8 3 basic_cmb.c:5 (parallel [ //
((reg59 * reg61) << 32)取高32位, (set
(reg:SI 60) //
再賦值給reg60 (truncate:SI
(lshiftrt:DI (mult:DI (sign_extend:DI (reg:SI 59)) (sign_extend:DI
(reg:SI 61))) (const_int
32 [0x20])))) //
即取reg59和reg61乘積的高32位,等效於imull指令 (clobber
(scratch:SI)) (clobber
(reg:CC 17 flags))]) -1 (nil)) (insn
8 7 9 3 basic_cmb.c:5 (parallel [ //
reg62 = reg60 >> 2 (set
(reg:SI 62) (ashiftrt:SI
(reg:SI 60) (const_int
2 [0x2]))) (clobber
(reg:CC 17 flags)) ])
-1 (nil)) (insn
9 8 10 3 basic_cmb.c:5 (parallel [ //
reg63 = reg59 >> 31 (set
(reg:SI 63) (ashiftrt:SI
(reg:SI 59) (const_int
31 [0x1f]))) (clobber
(reg:CC 17 flags)) ])
-1 (nil)) (insn
10 9 11 3 basic_cmb.c:5 (parallel [ (set
(reg:SI 58 [ D.1250 ]) //
reg58 = reg62 - reg63 (minus:SI
(reg:SI 62) (reg:SI
63))) (clobber
(reg:CC 17 flags)) ])
-1 (expr_list:REG_EQUAL ( div :SI
(reg:SI 59) //
表示reg59/10由上面的 (const_int
10 [0xa])) //reg58
= reg62 -reg63替代 (nil)))
//
即它們是等效的,從這個點也可以看出編譯器對除法做出的優化 (insn
11 10 12 3 basic_cmb.c:5 (set (mem/s/j:SI (plus:SI (mult:SI (reg:SI 58 [ D.1250 ])
//
reg58 * 4 + message = 42 (const_int
4 [0x4])) (symbol_ref:SI
( "message" )
)) [0 message S4 A32]) //其實這個已經和movl
$42, message(,%eax,4)相對應了 (const_int
42 [0x2a])) -1 (nil)) (insn
12 11 13 3 basic_cmb.c:6 (parallel [ //
__asm__ __volatile__ ("" ::: "memory");出現在這裏 (asm_operands/v
( "" )
( "" )
0 [] []
736) (clobber
(reg:QI 18 fpsr)) (clobber
(reg:QI 17 flags)) (clobber
(mem:BLK (scratch) [0 A8])) ])
-1 (nil)) (insn
13 12 18 3 basic_cmb.c:7 (set (mem/v/c/i:SI (symbol_ref:SI ( "ready" )
) [0 ready+0 S4 A32]) //
movl $1, ready (const_int
1 [0x1])) -1 (nil)) ;;
End of basic block 3 -> ( 4) ;;
Succ edge 4 (fallthru) ;;
Start of basic block ( 3) -> 4 ;;
Pred edge 3 (fallthru) (note
18 13 15 4 [bb 4] NOTE_INSN_BASIC_BLOCK) //
調試和說明信息,不用關心 (jump_insn
15 18 16 4 basic_cmb.c:8 (set (pc) //
goto label 17 (label_ref
17)) -1 (nil)) ;;
End of basic block 4 -> ( 6) ;;
Succ edge 6 (barrier
16 15 14) //
柵欄 ;;
Start of basic block () -> 5 (code_label
14 16 19 5 1 ""
[0 uses]) //
label 14 (note
19 14 17 5 [bb 5] NOTE_INSN_BASIC_BLOCK) //
調試和說明信息,不用關心 ;;
End of basic block 5 -> ( 6) ;;
Succ edge 6 (fallthru) ;;
Start of basic block ( 4 5) -> 6 ;;
Pred edge 4 ;;
Pred edge 5 (fallthru) (code_label
17 19 20 6 2 ""
[1 uses]) //
label 17 (note
20 17 0 6 [bb 6] NOTE_INSN_BASIC_BLOCK) //
調試和說明信息,不用關心 ;;
End of basic block 6 -> ( 1) ;;
Succ edge EXIT [100.0%] (fallthru) |
有關insns的介紹在http://www.lingcc.com/gccint/Insns.html#Insns
rtl文件有多個insn組成,insns表示的靈感即來自於LISP,當然,肯定不是規範的LISP語法,但至少”;;”作爲註釋這一點還是相同的,這裏大致介紹一下各個insns的作用:
- note用於表示額外的調試和說明信息
如:(note 1 0 3 NOTE_INSN_DELETED)
1 0 3分別表示當前insns的id爲1,上一條insns的id爲0,下一條爲3
NOTE_INSN_DELETED這樣的註解被完全忽略掉。編譯器的一些過程會通過將insn修改成這種類型的註解,來刪除insn。
本文並不是爲了講解RTL的,所以這裏只是簡單提一下,既然note是調試相關的信息,我們就暫時不關心它了。 - insn
表達式代碼insn用於不進行跳轉和函數調用的指令。sequence表達式總是包含在表達式代碼爲insn的insn中,即使它們中的一個insn是跳轉或者函數調用。
如:(insn 13 12 18 3 basic_cmb.c:7 (set (mem/v/c/i:SI (symbol_ref:SI (“ready”) ) [0 ready+0 S4 A32]) (const_int 1 [0x1])) -1 (nil))
13 12 18和note一樣,分別表示當前id,前一條id和後一條id;後面的3則表示所屬的基本塊id;
basic_cmb.c:7表示爲當前insns對應basic_cmb.c文件中的第7行;後面一大串內容要解釋起來也花上很大篇幅,可以參考http://www.lingcc.com/gccint/;不過應該還是比較容易看懂這句在幹嘛:set ready const_int 1,自然對應於ready=1的語句。 - barrier
柵欄被放在指令流中,控制無法經過的地方。它們被放在無條件跳轉指令的後面,表示跳轉是無條件的,以及對volatile函數的調用之後,表示不會返回(例如,exit)。除了三個標準的域以外,不包含其它信息。 - code_label即表示標籤label
- jump_insn
表達式代碼jump_insn用於可能執行跳轉(或者,更一般的講,指令中可能包含了label_ref表達式,並用其來設置pc)的指令。如果有一條從當前函數返回的指令,則其被記錄爲jump_insn。
如:(jump_insn 15 18 16 4 basic_cmb.c:8 (set (pc) (label_ref 17)) -1 (nil));設置pc寄存器爲17號label引用,即相當於goto label 17
從rtl中可以看到,每個insns通過當前id,前一條id和後一條id可以形成一條鏈,把所有insns鏈起來未免過長,由於我們的主要研究對象是Optimization barrier,所以就從insns11開始:
從圖中可以看到,在RTL這一層,我們的Optimization barrier還是存在的,所以可以大膽的猜測是在RTL優化的過程中它貢獻了自己的力量。
從RTL(Register Transfer Language)優化指令調度相關說明可以看到gcc實現部分相關代碼:haifa-sched.c, sched-deps.c, sched-ebb.c, sched-rgn.c和sched-vis.c。
先不用追根溯源,gcc大龐大,反而會摸不着頭腦的,Optimization barrier對應的insns爲:
1
2
3
4
5
6
7
|
(insn
12 11 13 3 basic_cmb.c:6 (parallel [ (asm_operands/v
( "" )
( "" )
0 [] []
736) (clobber
(reg:QI 18 fpsr)) (clobber
(reg:QI 17 flags)) (clobber
(mem:BLK (scratch) [0 A8])) ])
-1 (nil)) |
可以先參照http://www.lingcc.com/gccint/Side-Effects.html#Side-Effects
http://www.lingcc.com/gccint/Flags.html#Flags
不過,還是針對這條insn做一個較詳細的解釋吧:
- asm_operands:表這是一條asm操作指令
- /v:表含有volatile修飾符,所以asm_operands/v就和__asm__ __volatile__相對應
- reg:表寄存器
- QI:Quarter-Integer模式,表示一個一字節的整數
- fpsr:float point state register,表浮點狀態寄存器,所以reg:QI 18 fpsr表這是一個字節大小的浮點狀態寄存器,引用是18
- flags:通用狀態寄存器,所以reg:QI 17 flags表這是一個字節大小的通用狀態寄存器,引用是17
- mem:表內存
- BLK:“Block”模式,表示其它模式都不適用的聚合值
- (clobber x)
表示一個不可預期的存儲或者可能的存儲,將不可描述的值存儲到x,其必須爲一個reg,scratch, parallel 或者 mem表達式。
可以用在字符串指令中,將標準的值存儲到特定的硬件寄存器中。不需要去描述被存儲的值,只用來告訴編譯器寄存器被修改了,以免其嘗試在字符串指令中保持數據。如果x爲(mem:BLK (const_int 0))或者(mem:BLK (scratch)),則意味着所有的內存位置必須假設被破壞。
所以綜合來講,這條指令的意思是:volatile修飾的asm操作指令,並告知編譯器這條指令會改變狀態寄存器和內存,正好應對
1
|
__asm__
__volatile__ ( ""
::: "memory" ) |
針對這一點,我們還可以做一個實驗:
1
2
3
4
5
6
7
8
9
10
11
|
#include int
main( void ) { int
foo = 10, bar = 15; __asm__
__volatile__( "addl
%%ebx,%%eax" : "=a" (foo) : "a" (foo),
"b" (bar) ); printf ( "foo+bar=%d\n" ,
foo); return
0; } |
asm語句對應的insn爲(如對asm不熟悉,可以參照http://ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html):
1
2
3
4
5
6
7
8
9
10
11
12
13
|
(insn
9 8 10 asm.c:5 (parallel [ (set
(reg:SI 61) (asm_operands/v:SI
( "addl
%%ebx,%%eax" )
( "=a" )
0 [ (reg:SI
62) (reg:SI
63) ] [ (asm_input:SI
( "a" )
0) (asm_input:SI
( "b" )
0) ]
596358)) (clobber
(reg:QI 18 fpsr)) (clobber
(reg:QI 17 flags)) ])
-1 (nil)) |
這條insn更爲詳細,因爲未加”memory”所以,只默認告知編譯器狀態寄存器的改變。
我們再做一個實驗來應證這一點:
1
2
3
4
5
6
7
8
|
volatile
int
ready; int
message[100]; void
cmb ( int
i) { message[i/10]
= 42; __asm__
__volatile__ ( ""
::: "cc" ); ready
= 1; } |
將”memory”修改爲”cc”後,發現:
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
$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 |
這條語句是不起作用的,所以,是這個”memory”起到了關鍵作用,下面我們就重點關注”memory”產生的影響。