Skip to content

Commit bf2092c

Browse files
committed
Add function attributes; Move frame pointer elimination to function attributes section
1 parent 88a7ad5 commit bf2092c

File tree

2 files changed

+179
-123
lines changed

2 files changed

+179
-123
lines changed

src/05-控制流/02-函数.md

Lines changed: 0 additions & 123 deletions
Original file line numberDiff line numberDiff line change
@@ -297,126 +297,3 @@ dot cg.ll -Tpng -o cg.png
297297
生成如下图所示的函数调用图:
298298

299299
![Call Graph](../assets/05_cg.png)
300-
301-
### 帧指针清除优化
302-
303-
最后,再讲一个函数调用中的优化,就是帧指针清除优化(Frame Pointer Elimination)。
304-
305-
在讲这个之前,先讲一个比较小的优化。我们将一个非常简单的C程序
306-
307-
```c
308-
void foo(int a, int b) { }
309-
int main() {
310-
foo(1, 2);
311-
return 0;
312-
}
313-
```
314-
315-
编译为汇编程序,可以发现,`foo`函数的汇编代码为:
316-
317-
```x86asm
318-
foo:
319-
pushq %rbp
320-
movq %rsp, %rbp
321-
movl %edi, -4(%rbp)
322-
movl %esi, -8(%rbp)
323-
popq %rbp
324-
```
325-
326-
与我们常识有些违背。为啥这里栈不先增加(也就是对`rsp`寄存器进行`sub`),就直接把`edi`, `esi`的值移入栈内了呢?`-4(%rbp)``-8(%rbp)`的内存空间此刻似乎并不属于栈。
327-
328-
这是因为,在System V关于amd64架构的标准中,规定了`rsp`以下128个字节为red zone。这个区域,信号和异常处理函数均不会使用。因此,一个函数可以放心使用`rsp`以下128个字节的内容。
329-
330-
同时,我们对栈指针进行操作,一个很重要的原因就是为了进一步函数调用的时候,使用`call`指令会自动将被调用函数的返回地址压栈,那么就需要在调用`call`指令之前,保证栈顶指针确实指向栈顶,否则压栈就会覆盖一些数据。
331-
332-
但此时,我们的`foo`函数并没有调用别的函数,也就不会产生压栈行为。因此,如果在栈帧不超过128个字节的情况下,编译器自动为我们省去了这样的操作。为了验证这一点,我们做一个小的修改:
333-
334-
```c
335-
void bar() { }
336-
void foo(int a, int b) { bar(); }
337-
int main() {
338-
foo(1, 2);
339-
return 0;
340-
}
341-
```
342-
343-
这时,我们再看编译出的`foo`函数的汇编代码:
344-
345-
```x86asm
346-
foo:
347-
pushq %rbp
348-
movq %rsp, %rbp
349-
subq $16, %rsp
350-
movl %edi, -4(%rbp)
351-
movl %esi, -8(%rbp)
352-
callq bar
353-
addq $16, %rsp
354-
popq %rbp
355-
retq
356-
```
357-
358-
确实增加了对`rbp``sub``add`操作。而此时的`bar`函数,也没有对`rsp`的操作。
359-
360-
接下来,就要讲帧指针清除优化了。经过我们上述的讨论,一个函数在进入时会有一些固定动作:
361-
362-
1.`rbp`压栈
363-
2.`rsp`放入`rbp`
364-
3.`rsp`,预留栈空间
365-
366-
在函数返回之前,也有其相应的操作:
367-
368-
1.`rsp`,回收栈空间
369-
2.`rbp`最初的值弹栈回到`rbp`
370-
371-
我们刚刚讲的优化,使得没有调用别的函数的函数,可以省略掉进入时的第3步和返回前的第1步。那么,是否还可以继续省略呢?
372-
373-
那么,我们就要考虑为什么需要这些步骤。这些步骤都是围绕`rbp`进行的,而正是因为`rbp`经常进行这种操作,所以我们把`rbp`称为帧指针。之所以要进行这些操作,是因为我们在函数执行的过程中,栈顶指针随着不断调用别的函数,会不断移动,导致我们根据栈顶指针的位置,不太方便确定局部变量的位置。而如果我们在一开始就把`rsp`的值放在`rbp`中,那么局部变量的位置相对`rbp`是固定的,就更好确认了。注意到我们这里说根据`rsp`的值确认局部变量的位置只是不方便,但并不是不能做到。所以,我们可以增加一些编译器的负担,而把帧指针清除。
374-
375-
帧指针清除在LLVM IR层面其实十分方便,就是什么都不写。我们可以观察
376-
377-
```llvm
378-
define void @foo(i32 %a, i32 %b) {
379-
%1 = alloca i32
380-
%2 = alloca i32
381-
store i32 %a, ptr %1
382-
store i32 %b, ptr %2
383-
ret void
384-
}
385-
```
386-
387-
这个函数在编译成汇编语言之后,是:
388-
389-
```x86asm
390-
foo:
391-
movl %edi, -4(%rsp)
392-
movl %esi, -8(%rsp)
393-
retq
394-
```
395-
396-
不仅没有了栈的增加减少(之前提过的优化),也没有了对`rbp`的操作(帧指针清除)。
397-
398-
要想恢复这一操作也十分简单,在函数参数列表后加上一个属性`"frame-pointer"="all"`
399-
400-
```llvm
401-
define void @foo(i32 %a, i32 %b) "frame-pointer"="all" {
402-
%1 = alloca i32
403-
%2 = alloca i32
404-
store i32 %a, ptr %1
405-
store i32 %b, ptr %2
406-
ret void
407-
}
408-
```
409-
410-
其编译后的汇编程序就是:
411-
412-
```x86asm
413-
foo:
414-
pushq %rbp
415-
movq %rsp, %rbp
416-
movl %edi, -4(%rbp)
417-
movl %esi, -8(%rbp)
418-
popq %rbp
419-
retq
420-
```
421-
422-
恢复了往日的雄风。

src/06-内置函数、属性和元数据.md

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,182 @@ if (likely(x > 0)) {
5252
```
5353

5454
`__builtin_expect`这个内置指令,就会翻译为LLVM IR中的[`llvm.expect`](https://llvm.org/docs/LangRef.html#llvm-expect-intrinsic)内置函数,从而实现了静态分支预测。
55+
56+
## 属性
57+
58+
在C语言中,我们会遇到一个函数的修饰符:`inline`。这个修饰符会提示编译器,建议编译器在遇到这个函数的调用时,内联这个函数。这类的信息,LLVM会将其看作函数的「[属性](https://llvm.org/docs/LangRef.html#function-attributes)」(Attribtues)。
59+
60+
在之前,我们也提到过,我们可以:
61+
62+
```llvm
63+
define void @foo() attr1 attr2 attr3 {
64+
; ...
65+
}
66+
```
67+
68+
如果有多个函数有相同的属性,我们可以用一个属性组的形式来复用:
69+
70+
```llvm
71+
define void @foo1() #0 {
72+
; ...
73+
}
74+
define void @foo2() #0 {
75+
; ...
76+
}
77+
attributes #0 = { attr1 attr2 attr3 }
78+
```
79+
80+
LLVM支持的函数属性有多种,我们来看看几个比较容易理解的,由函数属性控制的优化:
81+
82+
### 内联
83+
84+
函数内联是一个非常复杂的概念,这里我们只是简单地来看一下,下面这个C语言代码:
85+
86+
```c
87+
inline int foo(int a) __attribute__((always_inline));
88+
89+
int foo(int a) {
90+
if (a > 0) {
91+
return a;
92+
} else {
93+
return 0;
94+
}
95+
}
96+
```
97+
98+
这里声明了`foo`函数,并且用了一个扩展语法`__attribute__((always_inline))`,这个语法实际上的作用就是给函数加上`alwaysinline`的属性。
99+
100+
我们查看其生成的LLVM IR:
101+
102+
```llvm
103+
define dso_local i32 @foo(i32 noundef %0) #0 {
104+
; ...
105+
}
106+
107+
attributes #0 = { alwaysinline nounwind uwtable "frame-pointer"="all" "min-legal-vector-width"="0" "no-trapping-math"="true" "stack-protector-buffer-size"="8" "target-cpu"="x86-64" "target-features"="+cx8,+fxsr,+mmx,+sse,+sse2,+x87" "tune-cpu"="generic" }
108+
```
109+
110+
可以看到,其确实有了`alwaysinline`这个属性。
111+
112+
### 帧指针清除优化
113+
114+
我们再来看一个属性控制的优化:帧指针清除优化(Frame Pointer Elimination)。
115+
116+
在讲这个之前,先讲一个比较小的优化。我们将一个非常简单的C程序
117+
118+
```c
119+
void foo(int a, int b) {}
120+
int main() {
121+
foo(1, 2);
122+
return 0;
123+
}
124+
```
125+
126+
编译为汇编程序,可以发现,`foo`函数的汇编代码为:
127+
128+
```x86asm
129+
foo:
130+
pushq %rbp
131+
movq %rsp, %rbp
132+
movl %edi, -4(%rbp)
133+
movl %esi, -8(%rbp)
134+
popq %rbp
135+
```
136+
137+
与我们常识有些违背。为啥这里栈不先增加(也就是对`rsp`寄存器进行`sub`),就直接把`edi`, `esi`的值移入栈内了呢?`-4(%rbp)``-8(%rbp)`的内存空间此刻似乎并不属于栈。
138+
139+
这是因为,在System V关于amd64架构的标准中,规定了`rsp`以下128个字节为red zone。这个区域,信号和异常处理函数均不会使用。因此,一个函数可以放心使用`rsp`以下128个字节的内容。
140+
141+
同时,我们对栈指针进行操作,一个很重要的原因就是为了进一步函数调用的时候,使用`call`指令会自动将被调用函数的返回地址压栈,那么就需要在调用`call`指令之前,保证栈顶指针确实指向栈顶,否则压栈就会覆盖一些数据。
142+
143+
但此时,我们的`foo`函数并没有调用别的函数,也就不会产生压栈行为。因此,如果在栈帧不超过128个字节的情况下,编译器自动为我们省去了这样的操作。为了验证这一点,我们做一个小的修改:
144+
145+
```c
146+
void bar() {}
147+
void foo(int a, int b) { bar(); }
148+
int main() {
149+
foo(1, 2);
150+
return 0;
151+
}
152+
```
153+
154+
这时,我们再看编译出的`foo`函数的汇编代码:
155+
156+
```x86asm
157+
foo:
158+
pushq %rbp
159+
movq %rsp, %rbp
160+
subq $16, %rsp
161+
movl %edi, -4(%rbp)
162+
movl %esi, -8(%rbp)
163+
callq bar
164+
addq $16, %rsp
165+
popq %rbp
166+
retq
167+
```
168+
169+
确实增加了对`rbp``sub``add`操作。而此时的`bar`函数,也没有对`rsp`的操作。
170+
171+
接下来,就要讲帧指针清除优化了。经过我们上述的讨论,一个函数在进入时会有一些固定动作:
172+
173+
1.`rbp`压栈
174+
2.`rsp`放入`rbp`
175+
3.`rsp`,预留栈空间
176+
177+
在函数返回之前,也有其相应的操作:
178+
179+
1.`rsp`,回收栈空间
180+
2.`rbp`最初的值弹栈回到`rbp`
181+
182+
我们刚刚讲的优化,使得没有调用别的函数的函数,可以省略掉进入时的第3步和返回前的第1步。那么,是否还可以继续省略呢?
183+
184+
那么,我们就要考虑为什么需要这些步骤。这些步骤都是围绕`rbp`进行的,而正是因为`rbp`经常进行这种操作,所以我们把`rbp`称为帧指针。之所以要进行这些操作,是因为我们在函数执行的过程中,栈顶指针随着不断调用别的函数,会不断移动,导致我们根据栈顶指针的位置,不太方便确定局部变量的位置。而如果我们在一开始就把`rsp`的值放在`rbp`中,那么局部变量的位置相对`rbp`是固定的,就更好确认了。注意到我们这里说根据`rsp`的值确认局部变量的位置只是不方便,但并不是不能做到。所以,我们可以增加一些编译器的负担,而把帧指针清除。
185+
186+
帧指针清除在LLVM IR层面其实十分方便,就是什么都不写。我们可以观察
187+
188+
```llvm
189+
define void @foo(i32 %a, i32 %b) {
190+
%1 = alloca i32
191+
%2 = alloca i32
192+
store i32 %a, ptr %1
193+
store i32 %b, ptr %2
194+
ret void
195+
}
196+
```
197+
198+
这个函数在编译成汇编语言之后,是:
199+
200+
```x86asm
201+
foo:
202+
movl %edi, -4(%rsp)
203+
movl %esi, -8(%rsp)
204+
retq
205+
```
206+
207+
不仅没有了栈的增加减少(之前提过的优化),也没有了对`rbp`的操作(帧指针清除)。
208+
209+
要想恢复这一操作也十分简单,在函数参数列表后加上一个属性`"frame-pointer"="all"`
210+
211+
```llvm
212+
define void @foo(i32 %a, i32 %b) "frame-pointer"="all" {
213+
%1 = alloca i32
214+
%2 = alloca i32
215+
store i32 %a, ptr %1
216+
store i32 %b, ptr %2
217+
ret void
218+
}
219+
```
220+
221+
其编译后的汇编程序就是:
222+
223+
```x86asm
224+
foo:
225+
pushq %rbp
226+
movq %rsp, %rbp
227+
movl %edi, -4(%rbp)
228+
movl %esi, -8(%rbp)
229+
popq %rbp
230+
retq
231+
```
232+
233+
恢复了往日的雄风。

0 commit comments

Comments
 (0)