March 6, 2020

编译器认知实验

实验目的和内容

本实验的目的是了解工业界常用的编译器 GCC 和 LLVM,熟悉编译器的安装和使用过程,观察编译器工作过程中生成的中间文件的格式和内容,了解编译器的优化效果,为编译器的学习和构造奠定基础。

本实验主要的内容为在 Linux 平台上安装和运行工业界常用的编译器 GCC 和 LLVM,编写简单的测试程序,使用编译器编译,并观察中间输出结果。

实现的具体过程和步骤

  • 编译器安装

  • 编写测试程序

  • 运行编译器进行观测

GCC 运行结果分析

  • 查看编译器的版本

    ❯ gcc --version
    gcc (GCC) 9.2.0
    Copyright © 2019 Free Software Foundation, Inc.
    本程序是自由软件;请参看源代码的版权声明。本软件没有任何担保;
    包括没有适销性和某一专用目的下的适用性担保。
  • 使用编译器编译单个文件

    gcc test.c

默认输出 a.out,加 -o 可以指定文件名

  • 使用编译器编译链接多个文件

我把 test.c 拆成了 test1.c 和 test2.c

test2.c

int a = 0;

test1.c

#include <stdio.h>
#define SECRET 42

extern int a;

int main() {
    a = 40;
    if (a + 2 == SECRET) {
        puts("yes");
    }
    return 0;
}

就是 2 里声明了 a 变量,1 里拿来用了。虽然简单,但的确是两个源码为一个程序服务。

编译成目标文件,链接目标文件为一个可执行文件的命令

gcc -c test1.c test2.c
gcc test1.o test2.o

最后生成的 a.out 运行正常,输出了 yes

  • 查看预处理结果:gcc -E hello.c -o hello.i

    # 1 "test.c"
    # 1 "<built-in>"
    # 1 "<命令行>"
    # 31 "<命令行>"
    # 1 "/usr/include/stdc-predef.h" 1 3 4
    # 32 "<命令行>" 2
    # 1 "test.c"
    # 1 "/usr/include/stdio.h" 1 3 4
    # 27 "/usr/include/stdio.h" 3 4
    # 1 "/usr/include/bits/libc-header-start.h" 1 3 4
    # 33 "/usr/include/bits/libc-header-start.h" 3 4
    # 1 "/usr/include/features.h" 1 3 4
    # 450 "/usr/include/features.h" 3 4
    # 1 "/usr/include/sys/cdefs.h" 1 3 4
    # 460 "/usr/include/sys/cdefs.h" 3 4
    # 1 "/usr/include/bits/wordsize.h" 1 3 4
    # 461 "/usr/include/sys/cdefs.h" 2 3 4
    # 1 "/usr/include/bits/long-double.h" 1 3 4
    # 462 "/usr/include/sys/cdefs.h" 2 3 4
    # 451 "/usr/include/features.h" 2 3 4
    # 474 "/usr/include/features.h" 3 4
    # 1 "/usr/include/gnu/stubs.h" 1 3 4
    # 10 "/usr/include/gnu/stubs.h" 3 4
    # 1 "/usr/include/gnu/stubs-64.h" 1 3 4
    # 11 "/usr/include/gnu/stubs.h" 2 3 4
    # 475 "/usr/include/features.h" 2 3 4
    # 34 "/usr/include/bits/libc-header-start.h" 2 3 4
    # 28 "/usr/include/stdio.h" 2 3 4

看来记录了文件名,读取了相关的头文件

  • 查看语法分析树:gcc -fdump-tree-all hello.c

老师说推荐分析.original、.gimple、.lower、.cfg 文件,所以用如下命令:

gcc -fdump-tree-{original, gimple, lower, cfg} test.c

根据文档,生成文件的数字是“遍”(pass)数,数字后的 t 代表 tree,最后的扩展名是遍的名字。所以我们可以知道文件生成的顺序。

test.c

#include <stdio.h>
#define SECRET 42

int main() {
    int a = 40;
    if (a + 2 == SECRET) {
        puts("yes");
    }
    return 0;
}

生成的中间“树”:

我们对比 if 那句,在 original 里基本没有改变,只是把大括号也往里缩进了。

到 gimple 里把 if 通过后的代码块用 goto 和标记改写了:

if (a == 40) goto <D.2312>; else goto <D.2313>;
<D.2312>:
puts ("yes");
<D.2313>:

从 gimple 到 lowerif 这句没有改变,有改变的是 lower 里消除了 gimple 里多嵌套的一对花括号和 return 语句。

到 cfg 里改变的主要是 goto 的标记:之前和 gimple 的中间变量一样,是“”的形式,现在是“”(中间变量仍然是 D 开头形式)

if (a == 40)
  goto <bb 3>; [INV]
else
  goto <bb 4>; [INV]

<bb 3> :
puts ("yes");

<bb 4> :
D.2314 = 0;

我感觉 tree 主要做的事就是用 goto 来表示流程。

  • 查看中间代码生成结果:Code generation result: gcc -fdump-rtl-all hello.c

这 rtl 根本就是 lisp!打开 lisp 的高亮,看起来舒服多了。

test.c 与上一个实验相同,我们和.expand 对比 if 语句(因为后面优化了怕差异太大)

rtl 基本上是汇编+无限多寄存器,所以汇编的一些指令和 rtl 指令很相似,比如跳转:rtl 里是jump_insn

expand 里总共只有一句跳转,所以 if 一定是它(其实”test.c”:6:8 也指明了,这一段是第 6 行):

(jump_insn 7 6 8 2 (set (pc)
        (if_then_else (ne (reg:CCZ 17 flags)
                (const_int 0 [0]))
            (label_ref 11)
            (pc))) "test.c":6:8 -1
     (nil)
 -> 11)

这句话相当于pc = r17 != 0? label(11): pc

所以说上一句肯定是把 test.c 里的判断结果放到了 r17 里:

(insn 6 5 7 2 (set (reg:CCZ 17 flags)
        (compare:CCZ (mem/c:SI (plus:DI (reg/f:DI 77 virtual-stack-vars)
                    (const_int -4 [0xfffffffffffffffc])) [1 a+0 S4 A32])
            (const_int 40 [0x28]))) "test.c":6:8 -1
     (nil))

mem/c……一直到 A32 那句可以理解成“当前栈顶-4”——也就是 main 函数的栈顶-4——得到变量 a

compare 拿 a 和 40 比,并且把结果通过 set 放到 r17 里。compare 应该对应汇编的 cmp,所以相等是 0,不等非 0。

回到 jump_insn,如果 r17 不为 0,就说明 a 不等于 40(对应 test.c 的 a+2!=42),跳到 label(11):

(code_label 11 10 12 5 2 (nil) [1 uses])

如果等于 40,就会顺序执行,接下来是一句 call 语句,这和汇编也很像:

(call_insn 10 9 11 4 (set (reg:SI 0 ax)
        (call (mem:QI (symbol_ref:DI ("puts") [flags 0x41]  <function_decl 0x7fbd3fe12200 puts>) [0 __builtin_puts S1 A8])
            (const_int 0 [0]))) "test.c":7:9 -1
     (nil)
    (expr_list:DI (use (reg:DI 5 di))
        (nil)))

很容易看清是从符号表(PLT)里找 puts 去调用

这一段执行之后就到了 label(11)了。

用前面 .cfg 的风格写一下这段 rtl 的伪代码:

if (compare (a, 40) != 0)
  goto label11;
else
  NULL

call ("puts");

label11:
……
  • 查看生成的目标代码(汇编代码):gcc –S hello.c –o hello.

看了这么长可能已经忘了一直在对比的源码了,这里我把它再贴出来:

if (a + 2 == SECRET) {
    puts("yes");
}
return 0;

汇编没有 rtl 那一堆括号清爽多了,if 语句对应的就是这几句:

	cmpl	$40, -4(%rbp)    ; 对应rtl里一长串virtual-stack-vars……-4
	jne	.L2                  ; 对应rtl里的jump_insn,gimple里的goto,实际程序的if
	leaq	.LC0(%rip), %rdi ; 把字符串地址放到寄存器里
	call	puts@PLT         ; 调用puts函数(从PLT里找),参数就是上一步的rdi
.L2:                         ; 这就是a != 40跳转的地方,源程序就return 0了
	movl	$0, %eax         ; 准备好0
	leave
	.cfi_def_cfa 7, 8
	ret                      ; 把0 return出去
	.cfi_endproc             ; 程序到此结束

LLVM 运行结果分析

  • 查看编译器的版本

    ❯ clang --version
    clang version 9.0.1
    Target: x86_64-pc-linux-gnu
    Thread model: posix
    InstalledDir: /usr/bin
  • 使用编译器编译单个文件

    clang test.c
  • 使用编译器编译链接多个文件

还是 gcc 时那两个文件,一样的操作,只不过把 gcc 换成 clang

  • 查看编译流程和阶段:clang -ccc-print-phases test.c -c

    ❯ clang -ccc-print-phases test.c -c
    0: input, "test.c", c
    1: preprocessor, {0}, cpp-output
    2: compiler, {1}, ir
    3: backend, {2}, assembler
    4: assembler, {3}, object
  • 查看词法分析结果:clang test.c -Xclang -dump-tokens -c

打印出来很长,因为它把 stdio 的代码也进行词法分析了,这里只截取 test.c 的:

int 'int'        [StartOfLine]  Loc=<test.c:4:1>
identifier 'main'        [LeadingSpace] Loc=<test.c:4:5>
l_paren '('             Loc=<test.c:4:9>
r_paren ')'             Loc=<test.c:4:10>
l_brace '{'      [LeadingSpace] Loc=<test.c:4:12>
int 'int'        [StartOfLine] [LeadingSpace]   Loc=<test.c:5:5>
identifier 'a'   [LeadingSpace] Loc=<test.c:5:9>
equal '='        [LeadingSpace] Loc=<test.c:5:11>
numeric_constant '40'    [LeadingSpace] Loc=<test.c:5:13>
semi ';'                Loc=<test.c:5:15>
if 'if'  [StartOfLine] [LeadingSpace]   Loc=<test.c:6:5>
l_paren '('      [LeadingSpace] Loc=<test.c:6:8>
identifier 'a'          Loc=<test.c:6:9>
plus '+'         [LeadingSpace] Loc=<test.c:6:11>
numeric_constant '2'     [LeadingSpace] Loc=<test.c:6:13>
equalequal '=='  [LeadingSpace] Loc=<test.c:6:15>
numeric_constant '42'    [LeadingSpace] Loc=<test.c:6:18 <Spelling=test.c:2:16>>
r_paren ')'             Loc=<test.c:6:24>
l_brace '{'      [LeadingSpace] Loc=<test.c:6:26>
identifier 'puts'        [StartOfLine] [LeadingSpace]   Loc=<test.c:7:9>
l_paren '('             Loc=<test.c:7:13>
string_literal '"yes"'          Loc=<test.c:7:14>
r_paren ')'             Loc=<test.c:7:19>
semi ';'                Loc=<test.c:7:20>
r_brace '}'      [StartOfLine] [LeadingSpace]   Loc=<test.c:8:5>
return 'return'  [StartOfLine] [LeadingSpace]   Loc=<test.c:9:5>
numeric_constant '0'     [LeadingSpace] Loc=<test.c:9:12>
semi ';'                Loc=<test.c:9:13>
r_brace '}'      [StartOfLine]  Loc=<test.c:10:1>
eof ''          Loc=<test.c:10:2>

看得出是:类型+实际字符串+无用字符+文件中位置的格式

if 那句就是:

if 'if'  [StartOfLine] [LeadingSpace]   Loc=<test.c:6:5>
l_paren '('      [LeadingSpace] Loc=<test.c:6:8>
identifier 'a'          Loc=<test.c:6:9>
plus '+'         [LeadingSpace] Loc=<test.c:6:11>
numeric_constant '2'     [LeadingSpace] Loc=<test.c:6:13>
equalequal '=='  [LeadingSpace] Loc=<test.c:6:15>
numeric_constant '42'    [LeadingSpace] Loc=<test.c:6:18 <Spelling=test.c:2:16>>
r_paren ')'             Loc=<test.c:6:24>

这比 gcc 莫名其妙的“tree”更贴近教材。

  • 查看词法分析结果 2:clang test.c -Xclang -dump-raw-tokens -c

    hash '#'         [StartOfLine]  Loc=<test.c:1:1>
    raw_identifier 'include'                Loc=<test.c:1:2>
    unknown ' '             Loc=<test.c:1:9>
    less '<'                Loc=<test.c:1:10>
    raw_identifier 'stdio'          Loc=<test.c:1:11>
    period '.'              Loc=<test.c:1:16>
    raw_identifier 'h'              Loc=<test.c:1:17>
    greater '>'             Loc=<test.c:1:18>
    unknown '
    '               Loc=<test.c:1:19>
    hash '#'         [StartOfLine]  Loc=<test.c:2:1>
    raw_identifier 'define'         Loc=<test.c:2:2>
    unknown ' '             Loc=<test.c:2:8>
    raw_identifier 'SECRET'         Loc=<test.c:2:9>
    unknown ' '             Loc=<test.c:2:15>
    numeric_constant '42'           Loc=<test.c:2:16>
    unknown '
    
    '               Loc=<test.c:2:18>
    raw_identifier 'int'     [StartOfLine]  Loc=<test.c:4:1>
    unknown ' '             Loc=<test.c:4:4>
    raw_identifier 'main'           Loc=<test.c:4:5>
    l_paren '('             Loc=<test.c:4:9>
    r_paren ')'             Loc=<test.c:4:10>
    unknown ' '             Loc=<test.c:4:11>
    l_brace '{'             Loc=<test.c:4:12>
    unknown '
    '           Loc=<test.c:4:13>
    raw_identifier 'int'     [StartOfLine]  Loc=<test.c:5:5>
    unknown ' '             Loc=<test.c:5:8>
    raw_identifier 'a'              Loc=<test.c:5:9>
    unknown ' '             Loc=<test.c:5:10>
    equal '='               Loc=<test.c:5:11>
    unknown ' '             Loc=<test.c:5:12>
    numeric_constant '40'           Loc=<test.c:5:13>
    semi ';'                Loc=<test.c:5:15>
    unknown '
    '           Loc=<test.c:5:16>
    raw_identifier 'if'      [StartOfLine]  Loc=<test.c:6:5>
    unknown ' '             Loc=<test.c:6:7>
    l_paren '('             Loc=<test.c:6:8>
    raw_identifier 'a'              Loc=<test.c:6:9>
    unknown ' '             Loc=<test.c:6:10>
    plus '+'                Loc=<test.c:6:11>
    unknown ' '             Loc=<test.c:6:12>
    numeric_constant '2'            Loc=<test.c:6:13>
    unknown ' '             Loc=<test.c:6:14>
    equalequal '=='         Loc=<test.c:6:15>
    unknown ' '             Loc=<test.c:6:17>
    raw_identifier 'SECRET'         Loc=<test.c:6:18>
    r_paren ')'             Loc=<test.c:6:24>
    unknown ' '             Loc=<test.c:6:25>
    l_brace '{'             Loc=<test.c:6:26>
    unknown '
        '               Loc=<test.c:6:27>
    raw_identifier 'puts'    [StartOfLine]  Loc=<test.c:7:9>
    l_paren '('             Loc=<test.c:7:13>
    string_literal '"yes"'          Loc=<test.c:7:14>
    r_paren ')'             Loc=<test.c:7:19>
    semi ';'                Loc=<test.c:7:20>
    unknown '
    '           Loc=<test.c:7:21>
    r_brace '}'      [StartOfLine]  Loc=<test.c:8:5>
    unknown '
    '           Loc=<test.c:8:6>
    raw_identifier 'return'  [StartOfLine]  Loc=<test.c:9:5>
    unknown ' '             Loc=<test.c:9:11>
    numeric_constant '0'            Loc=<test.c:9:12>
    semi ';'                Loc=<test.c:9:13>
    unknown '
    '               Loc=<test.c:9:14>
    r_brace '}'      [StartOfLine]  Loc=<test.c:10:1>

首先 raw-tokens 不会去引用的库里读代码。

其次分析出来的类型也只是字符串本身,没经过归类的属性。比如将空格分类给“unknown”,上一个实验里它就是“[LeadingSpace]”,还有 include 的尖括号,这里也直接分析成大于小于号

if 那句和上面类似,只不过叫成了“raw_identifier ‘if’”

  • 查看语义分析结果:clang test.c -Xclang -ast-dump -c

很好看,是彩色的:

同样这里只取 main 本身的

此时从 token stream 里分析出了具体含义,比如把

int 'int'
identifier 'main'
l_paren '('
r_paren ')'
l_brace '{'
……
r_brace '}'

转换成了 FunctionDecl……main……,而且是用树的形式打印的,作用域什么的一目了然(再瞧瞧 gcc 那所谓的“tree”,里头竟然全是用 goto 分割树枝的代码)

`-FunctionDecl 0x5607f994d090 <test.c:4:1, line:10:1> line:4:5 main 'int ()'
  `-CompoundStmt 0x5607f994d3d8 <col:12, line:10:1>
    |-DeclStmt 0x5607f994d1d0 <line:5:5, col:15>
    | `-VarDecl 0x5607f994d148 <col:5, col:13> col:9 used a 'int' cinit
    |   `-IntegerLiteral 0x5607f994d1b0 <col:13> 'int' 40
    |-IfStmt 0x5607f994d390 <line:6:5, line:8:5>
    | |-BinaryOperator 0x5607f994d280 <line:6:9, line:2:16> 'int' '=='
    | | |-BinaryOperator 0x5607f994d240 <line:6:9, col:13> 'int' '+'
    | | | |-ImplicitCastExpr 0x5607f994d228 <col:9> 'int' <LValueToRValue>
    | | | | `-DeclRefExpr 0x5607f994d1e8 <col:9> 'int' lvalue Var 0x5607f994d148 'a' 'int'
    | | | `-IntegerLiteral 0x5607f994d208 <col:13> 'int' 2
    | | `-IntegerLiteral 0x5607f994d260 <line:2:16> 'int' 42
    | `-CompoundStmt 0x5607f994d378 <line:6:26, line:8:5>
    |   `-CallExpr 0x5607f994d320 <line:7:9, col:19> 'int'
    |     |-ImplicitCastExpr 0x5607f994d308 <col:9> 'int (*)(const char *)' <FunctionToPointerDecay>
    |     | `-DeclRefExpr 0x5607f994d2a0 <col:9> 'int (const char *)' Function 0x5607f9948740 'puts' 'int (const char *)'
    |     `-ImplicitCastExpr 0x5607f994d360 <col:14> 'const char *' <NoOp>
    |       `-ImplicitCastExpr 0x5607f994d348 <col:14> 'char *' <ArrayToPointerDecay>
    |         `-StringLiteral 0x5607f994d2c0 <col:14> 'char [4]' lvalue "yes"
    `-ReturnStmt 0x5607f994d3c8 <line:9:5, col:12>
      `-IntegerLiteral 0x5607f994d3a8 <col:12> 'int' 0

显然我们一直在对比的 if 就是IfStmt以及它下面那些 Operator、Expr、Stmt、Literal 什么的

  • 查看编译优化的结果:clang test.c -S -mllvm -print-after-all

控制台输出了一大堆中间代码,而且是向 stderr 输出的,为什么不是 stdout???

前些个 pass 的输出还比较可读,我选了第一个和源码比较

*** IR Dump After Instrument function entry/exit with calls to e.g. mcount() (pre inlining) ***
; Function Attrs: noinline nounwind optnone sspstrong uwtable
define dso_local i32 @main() #0 {
  %1 = alloca i32, align 4
  %2 = alloca i32, align 4
  store i32 0, i32* %1, align 4
  store i32 40, i32* %2, align 4
  %3 = load i32, i32* %2, align 4
  %4 = add nsw i32 %3, 2
  %5 = icmp eq i32 %4, 42
  br i1 %5, label %6, label %8

6:                                                ; preds = %0
  %7 = call i32 @puts(i8* getelementptr inbounds ([4 x i8], [4 x i8]* @.str, i64 0, i64 0))
  br label %8

8:                                                ; preds = %6, %0
  ret i32 0
}

这应该是 llvm ir,很明显define dso_local i32 @main()这一句是定义返回值是 int32 的 main 函数。

llvm ir 里一切数据都要指定类型与大小,就比如说这些 i32;变量用“%”开头,命令都是直接写,比如“store”

main 里前四句是申请两个 i32 的内存,一个放 0 一个放 40(变量 a)

%3 = load i32, i32* %2, align 4把 a 读入(这个 store、load 像 risc 诶)

%4 = add nsw i32 %3, 2给 a 加 2,然后%5 = icmp eq i32 %4, 42比较结果等不等于 42(看来 llvm ir 并没有明确的寄存器,都是通过=赋值的,变成寄存器应该是之后才优化)

br i1 %5, label %6, label %8就是 if-else 了:true 就 goto 到 label6,也就是 call puts 那句;否则是 label8,直接 return 0。

到最后就变成对寄存器操作了:

# *** IR Dump After Check CFA info and insert CFI instructions if needed ***:
# Machine code for function main: NoPHIs, TracksLiveness, NoVRegs
Frame Objects:
  fi#-1: size=8, align=16, fixed, at location [SP-8]
  fi#0: size=4, align=4, at location [SP-12]
  fi#1: size=4, align=4, at location [SP-16]

bb.0 (%ir-block.0):
  successors: %bb.2, %bb.1

  frame-setup PUSH64r killed $rbp, implicit-def $rsp, implicit $rsp
  CFI_INSTRUCTION def_cfa_offset 16
  CFI_INSTRUCTION offset $rbp, -16
  $rbp = frame-setup MOV64rr $rsp
  CFI_INSTRUCTION def_cfa_register $rbp
  $rsp = frame-setup SUB64ri8 $rsp(tied-def 0), 16, implicit-def dead $eflags
  MOV32mi $rbp, 1, $noreg, -4, $noreg, 0 :: (store 4 into %ir.1)
  MOV32mi $rbp, 1, $noreg, -8, $noreg, 40 :: (store 4 into %ir.2)
  renamable $eax = MOV32rm $rbp, 1, $noreg, -8, $noreg :: (load 4 from %ir.2)
  renamable $eax = ADD32ri8 renamable $eax(tied-def 0), 2, implicit-def $eflags
  CMP32ri8 killed renamable $eax, 42, implicit-def $eflags
  JCC_1 %bb.2, 5, implicit $eflags

bb.1 (%ir-block.6):
; predecessors: %bb.0
  successors: %bb.2

  renamable $rdi = LEA64r $rip, 1, $noreg, @.str, $noreg
  CALL64pcrel32 target-flags(x86-plt) @puts, <regmask $bh $bl $bp $bph $bpl $bx $ebp $ebx $hbp $hbx $rbp $rbx $r12 $r13 $r14 $r15 $r12b $r13b $r14b $r15b $r12bh $r13bh $r14bh $r15bh $r12d $r13d $r14d $r15d $r12w $r13w $r14w $r15w $r12wh and 3 more...>, implicit $rsp, implicit $ssp, implicit killed $rdi, implicit-def $eax

bb.2 (%ir-block.8):
; predecessors: %bb.0, %bb.1

  renamable $eax = XOR32rr undef $eax(tied-def 0), undef $eax, implicit-def $eflags
  $rsp = frame-destroy ADD64ri8 $rsp(tied-def 0), 16, implicit-def dead $eflags
  $rbp = frame-destroy POP64r implicit-def $rsp, implicit $rsp
  CFI_INSTRUCTION def_cfa $rsp, 8
  RETQ implicit killed $eax

# End machine code for function main.

可以看到$rbp呀,$eax 呀之类的寄存器出现,另外括号里有“%ir”的也像注释一样告诉你这对应 ir 的哪一句,比如MOV32mi $rbp, 1, $noreg, -8, $noreg, 40 :: (store 4 into %ir.2)就是store i32 40, i32* %2, align 4把 40 放进第二个变量里。

CMP32ri8 killed renamable $eax, 42, implicit-def $eflags是比较源码里的 a 和 42,下一句JCC_1就是跳转(Jump Condition Code 1?)

不跳转的话会执行CALL64pcrel32……调用 puts;跳转的话就到bb.2(也写了,对应 ir 里的 label8)退栈(frame-destroy),返回。

这个实验说是叫“查看编译优化的结果”,但其实到最后都还是判断 a+2==42,gcc 一上来到 original 就是判断 a==40 了。不过这也是因为测试程序太短了,所以出现的可优化特例没有被优化吧。

  • 查看生成的目标代码结果:Target code generation:clang -S test.c

其实上一步就生成了汇编代码了,只不过这个命令不会往控制台打印很多东西。

生成的汇编文件中与 if 有关的是这一段:

	movl	$40, -8(%rbp)      ; 很奇怪,clang是用基址寄存器定位变量的
	movl	-8(%rbp), %eax     ; 把a放到eax里
	addl	$2, %eax           ; 给a加2
	cmpl	$42, %eax          ; a和42比大小——这里gcc从**一开始**就优化掉了,直接跳过+2和40比大小
	jne	.LBB0_2              ; if语句来啦
# %bb.1:
	leaq	.L.str(%rip), %rdi ; 把字符串放到rdi
	callq	puts@PLT           ; 调用puts
.LBB0_2:
	xorl	%eax, %eax         ; 清零eax,gcc用的是mov 0
	addq	$16, %rsp
	popq	%rbp               ; 这两句对应gcc的leave
	.cfi_def_cfa %rsp, 8
	retq

GCC 与 LLVM 对比分析

CPU 是英特尔 i5-7200U,4 核 2.5GHz, 4 个 32KB L1 Cache, 两个 256KB L2 Cache, 一个 3M L3 Cache. 内存 16GB。

我用 python 生成了 1000001 个 0-32767 的随机整数做输入列表,通过cat input.txt | ./a.out输入, 程序内部记录排序函数的时间。每种优化程度计时 6 次取平均。

代码是 lab1 的未调优快排,表格单位是秒

O0O1O2O3
gcc0.2116040.1022690.0837050.087721
clang0.1987860.1014470.0707810.070321

可以看到不管何种程度的优化,这两种编译器编译出的程序速度都很接近,只不过 clang 一直比 gcc 快

而且只有最初的优化消减的时间多(O0 到 O1 有 0.1 秒,O1 到 O2 只有 0.02 秒),奇怪的是 gcc 的 O3 比 O2 还慢,是它优化太多把自己绕晕了?

实验心得体会

两个编译器都是“前后端分离”的:前段吐出语言无关、平台无关的中间代码,后端把中间代码翻译成(汇编、)机器码。

还有两个编译器都有很多 pass,我原以为最多扫三遍就够了,工业级编译器真是不可小觑。clang -print-after-all 的输出有 37 个 after,大概有 37 个 pass 吧,gcc 更狠,到 dfinish 有 318 个 pass。我说 c 编译怎么那么慢呢,听说 go 很快,应该没多少 pass 吧。

对于中间代码来说,clang 比 gcc 更可读:clang 的 ir 类似 risc 汇编,一条一条的简短但信息量足(指区分类型,变量、指令、寄存器的前缀命名),对应到 gcc 每一句都是好几行罗列好多奇怪数字的 lisp rtl。而且 llvm ir 有变量的概念,gcc 的 rtl 取一个变量都要通过栈顶加常量来获取——这一段就要两行+一万个括号。

而且 clang 也更贴近教科书,或者说“调理更清晰”;gcc 像是没怎么设计,总之能跑能用就得了的程序:对于语法树,clang 真的是以树形图打印的,gcc 就是一团代码充斥着 goto;中间代码,clang 好歹能看,gcc 就是拿来 lisp 写了一堆宏/函数发现能用就接着用了。

我以后写 c/c++ 都会尽量用 clang 了,这次实验 gcc 给我的印象太差了:又慢又混乱。至于 Windows 上,我记得几年前 chrome 也改成 clang 了。

另外虽然我可能不会接触,但如果要二次开发的话,clang 是更好的选择,因为中间代码清爽许多。

如无特殊声明,本页内容采用 CC BY-NC 4.0 授权


Made With Notepad