概述
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=%dn"
, 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”产生的影响。
最后
以上就是活力饼干为你收集整理的优化屏障(Optimization barrier)第二讲 1. gcc编译的大致过程 2. 从RTL指令调度出发,追寻Optimization barrier的踪迹的全部内容,希望文章能够帮你解决优化屏障(Optimization barrier)第二讲 1. gcc编译的大致过程 2. 从RTL指令调度出发,追寻Optimization barrier的踪迹所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复