看这段简单但不完全平凡的代码:

1
2
3
4
5
6
7
8
9
10
11
// Filename: 0.c
void f(int n) {
    f(++n);
}

int main() {
    int i = 0;
    for(;;++i) {
        f(i*i);
    }
}

如果用 clang 0.c 编译,程序会很快因为 f 这个函数无限递归导致栈溢出而终止。

如果用 clang -O3 0.c 编译,则程序会一直停留在死循环里,不会终止。

未经优化得到的汇编代码是

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
	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 11
	.globl	_f
	.align	4, 0x90
_f:                                     ## @f
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp0:
	.cfi_def_cfa_offset 16
Ltmp1:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp2:
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movl	%edi, -4(%rbp)
	movl	-4(%rbp), %edi
	addl	$1, %edi
	movl	%edi, -4(%rbp)
	callq	_f
	addq	$16, %rsp
	popq	%rbp
	retq
	.cfi_endproc

	.globl	_main
	.align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp3:
	.cfi_def_cfa_offset 16
Ltmp4:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp5:
	.cfi_def_cfa_register %rbp
	subq	$16, %rsp
	movl	$0, -4(%rbp)
	movl	$0, -8(%rbp)
LBB1_1:                                 ## =>This Inner Loop Header: Depth=1
	movl	-8(%rbp), %eax
	imull	-8(%rbp), %eax
	movl	%eax, %edi
	callq	_f
## BB#2:                                ##   in Loop: Header=BB1_1 Depth=1
	movl	-8(%rbp), %eax
	addl	$1, %eax
	movl	%eax, -8(%rbp)
	jmp	LBB1_1
	.cfi_endproc


.subsections_via_symbols

O3 优化后对应的汇编代码是

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
	.section	__TEXT,__text,regular,pure_instructions
	.macosx_version_min 10, 11
	.globl	_f
	.align	4, 0x90
_f:                                     ## @f
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp0:
	.cfi_def_cfa_offset 16
Ltmp1:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp2:
	.cfi_def_cfa_register %rbp
	popq	%rbp
	retq
	.cfi_endproc

	.globl	_main
	.align	4, 0x90
_main:                                  ## @main
	.cfi_startproc
## BB#0:
	pushq	%rbp
Ltmp3:
	.cfi_def_cfa_offset 16
Ltmp4:
	.cfi_offset %rbp, -16
	movq	%rsp, %rbp
Ltmp5:
	.cfi_def_cfa_register %rbp
	.align	4, 0x90
LBB1_1:                                 ## =>This Inner Loop Header: Depth=1
	jmp	LBB1_1
	.cfi_endproc


.subsections_via_symbols

可见优化的版本里最后执行的是一个简单的死循环 (34-35 行),对函数 f 的调用被完全移除了,尽管还是“假模假式”地定义了 f (什么功能都没有,也没有递归调用的部分)。

至于 gcc (clang) 是怎么推断出 f 这个函数虽然在做事,但做的事是“封闭的”、对外界没有“逻辑上的”副作用 (尽管有事实上的副作用,那就是栈溢出),可以在这里看到一些端倪。想了想,也许是通过把递归转换为循环可以看出 f 这个函数没有副作用,所以就移除了。

那个页面提到 LLVM 会为了方便后续分析而把

1
for (i = 7; i*i < 1000; ++i)

变成

1
for (i = 0; i != 25; ++i)

真没想到它会做这样的变换。