優化屏障(Optimization barrier)第二講

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”產生的影響。

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