GCC编译优化和调试选项

  1. 1. GCC优化选项
    1. -O/-O1
    2. -O2
    3. -O3
    4. -O0
    5. -Os
    6. -Ofast
    7. -Og
  2. 2. GCC 调试选项
    1. -g
    2. -ggdb
    3. -gdwarf
    4. -gstabs
    5. -glevel
  3. 3. 参考

没有开启编译优化时,GCC编译器的目的是:减少编译时间生成预期的调试结果。对于GCC编译的程序,调试的语句都是独立的,可以在程序的任何语句中设置断点,并设置变量的值和修改语句的执行,得到你想要的执行结果。

开启编译优化的开关时,GCC编译器的目的是:优化程序的性能减少代码的大小,尽管会以牺牲编译时间和程序的可调试能力为代价。

编译器对于程序的优化处理是基于编译期间对程序分析。不是所有的优化都是通过编译选项来控制的。这里只介绍一些有编译选项的优化列表。在编译选项-O0或者-O下很多的优化是关闭的,即使设置独立的编译优化选项,例如-Og,也省略了很多优化过程。对于不同的优化级别开启的对应优化开关可以通过gcc -Q -O2 --help=optimizers来查看对应的开启优化列表。

1. GCC优化选项

-O/-O1

这两个都是开启level 1的编译优化。开启编译优化会导致更长的编译时间,对于大函数还会消耗更多的内存空间。level1的编译优化下,编译器会尝试减少代码段大小和优化程序的执行时间但不执行需要消耗大量编译时间的优化

由此可见level 1级别的优化,只是有限的优化代码大小和执行时间,不会为了优化,而带来编译时间的巨大变化。Level 1级别的优化下,可以通过gcc -Q -O1 --help=optimizers查看开启的编译开关,下面是本机**gcc version 7.4.0 (ubuntu1~18.04.1)**环境下,O1开启的优化选项:

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
-faggressive-loop-optimizations [enabled]
-fasynchronous-unwind-tables [enabled]
// 将地址的递增和递减与存储器的访问相结合
-fauto-inc-dec [enabled]
// 扫描是否有机会在计数寄存器上使用“递减和分支”指令,而不是对寄存器进行递减,然后与0比较,并根据结果进行分支指令的执行。
-fbranch-count-reg [enabled]
// 跟踪堆栈的调整(压入和弹出)和栈内存的引用,尝试组合它们
-fcombine-stack-adjustments [enabled]
-fcompare-elim [enabled]
-fcprop-registers [enabled]
-fdce [enabled]
-fdefer-pop [enabled]
-fdelete-null-pointer-checks [enabled]
-fdse [enabled]
-fearly-inlining [enabled]
-fforward-propagate [enabled]
-ffp-int-builtin-inexact [enabled]
-ffunction-cse [enabled]
-fgcse-lm [enabled]
-fguess-branch-probability [enabled]
-fif-conversion [enabled]
-fif-conversion2 [enabled]
// 尝试对所有函数进行内联,在不开启优化时,即fno-inline时,除了用ALWAYS_INLINE属性标记的函数,编译器不会进行任何内联操作。函数可以通过noinline属性来标记不进行内联操作。
-finline [enabled]
-finline-atomics [enabled]
// 将所有只调用一次的static函数,内联到调用处,即使没有标记为inline,如果该调用函数被内联后,该函数本身不会输出汇编代码
-finline-functions-called-once [enabled]
-fipa-profile [enabled]
-fipa-pure-const [enabled]
-fipa-reference [enabled]
-fira-hoist-pressure [enabled]
-fira-share-save-slots [enabled]
-fira-share-spill-slots [enabled]
-fivopts [enabled]
-fjump-tables [enabled]
-flifetime-dse [enabled]
-fmath-errno [enabled]
-fmove-loop-invariants [enabled]
// 忽略栈帧指针,这是一个很糟糕的优化,会在profiling的时候丢失调用关系链
-fomit-frame-pointer [enabled]
-fpeephole [enabled]
-fplt [enabled]
-fprefetch-loop-arrays [enabled]
-fprintf-return-value [enabled]
-freg-struct-return [enabled]
-frename-registers [enabled]
-freorder-blocks [enabled]
-frtti [enabled]
-fsched-critical-path-heuristic [enabled]
-fsched-dep-count-heuristic [enabled]
-fsched-group-heuristic [enabled]
-fsched-interblock [enabled]
-fsched-last-insn-heuristic [enabled]
-fsched-rank-heuristic [enabled]
-fsched-spec [enabled]
-fsched-spec-insn-heuristic [enabled]
-fsched-stalled-insns-dep [enabled]
-fschedule-fusion [enabled]
-fshort-enums [enabled]
-fshrink-wrap [enabled]
-fshrink-wrap-separate [enabled]
-fsigned-zeros [enabled]
-fsplit-ivs-in-unroller [enabled]
-fsplit-wide-types [enabled]
-fssa-backprop [enabled]
-fssa-phiopt [enabled]
-fstdarg-opt [enabled]
-fstrict-volatile-bitfields [enabled]
-fno-threadsafe-statics [enabled]
-ftrapping-math [enabled]
-ftree-bit-ccp [enabled]
-ftree-builtin-call-dce [enabled]
-ftree-ccp [enabled]
-ftree-ch [enabled]
-ftree-coalesce-vars [enabled]
-ftree-copy-prop [enabled]
-ftree-cselim [enabled]
-ftree-dce [enabled]
-ftree-dominator-opts [enabled]
-ftree-dse [enabled]
-ftree-forwprop [enabled]
-ftree-fre [enabled]
-ftree-loop-if-convert [enabled]
-ftree-loop-im [enabled]
-ftree-loop-ivcanon [enabled]
-ftree-loop-optimize [enabled]
-ftree-phiprop [enabled]
-ftree-pta [enabled]
-ftree-reassoc [enabled]
-ftree-scev-cprop [enabled]
-ftree-sink [enabled]
-ftree-slsr [enabled]
-ftree-sra [enabled]
-ftree-ter [enabled]
-funwind-tables [enabled]
-fvar-tracking [enabled]
-fvar-tracking-assignments [enabled]
-fweb [enabled]

-O2

相比于-O1,-O2打开了更多的编译优化开关。level2级别的优化选项相对于-O,牺牲了更多编译时间,但提高了程序的性能。以下是本机**gcc version 7.4.0 (ubuntu1~18.04.1)**环境下,O2相对于O1额外开启的优化选项:

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
-falign-labels              		[enabled]
-fcaller-saves [enabled]
-fcode-hoisting [enabled]
-fcrossjumping [enabled]
-fcse-follow-jumps [enabled]
-fdevirtualize [enabled]
-fdevirtualize-speculatively [enabled]
-fexpensive-optimizations [enabled]
-fgcse [enabled]
-fhoist-adjacent-loads [enabled]
// 尝试对间接调用进行内联,编译器会根据最外层的内联来决定是否会进行内部的间接内联,此选项生效的前提是-finline-functions或-finline-small-functions选项的开启
-findirect-inlining [enabled]
// 将小函数内联到调用处,编译器会根据函数体大小计算内联后整体程序打大小,来决定是否内联。这种内联操作会针对所有函数,即使没有申明inline的函数
-finline-small-functions [enabled]
-fipa-bit-cp [enabled]
-fipa-cp [enabled]
-fipa-icf [enabled]
-fipa-icf-functions [enabled]
-fipa-icf-variables [enabled]
-fipa-ra [enabled]
-fipa-sra [enabled]
-fipa-vrp [enabled]
-fisolate-erroneous-paths-dereference [enabled]
-flra-remat [enabled]
-foptimize-sibling-calls [enabled]
-foptimize-strlen [enabled]
-fpartial-inlining [enabled]
-fpeephole2 [enabled]
-freorder-blocks-and-partition [enabled]
-freorder-functions [enabled]
-frerun-cse-after-loop [enabled]
-fschedule-insns2 [enabled]
-fstore-merging [enabled]
// 限制不同类型的变量名指向同一块内存,一般这个选项都是关闭的,因为很多时候我们会进行强制装换,所以不开启很多代码库会编译不过
-fstrict-aliasing [enabled]
-fstrict-overflow [enabled]
-fthread-jumps [enabled]
-ftree-pre [enabled]
-ftree-switch-conversion [enabled]
-ftree-tail-merge [enabled]
-ftree-vrp [enabled]

-O3

在-O2的基础上,level 3的级别优化,以下是本机**gcc version 7.4.0 (ubuntu1~18.04.1)**环境下,额外开启了以下优化开关:

1
2
3
4
5
6
7
8
9
10
11
12
-fgcse-after-reload         		[enabled]
-finline-functions [enabled]
-fipa-cp-clone [enabled]
-fpeel-loops [enabled]
-fpredictive-commoning [enabled]
-fsplit-loops [enabled]
-fsplit-paths [enabled]
-ftree-loop-distribute-patterns [enabled]
-ftree-loop-vectorize [enabled]
-ftree-partial-pre [enabled]
-ftree-slp-vectorize [enabled]
-funswitch-loops [enabled]

-O0

默认的优化选项,减少编译时间和生成完整的调试信息。

-Os

优化生成的目标文件的大小。-Os是基于-O2的优化选项,按照手册的说法:

-Os开启了-O2优化选项的所有开关,除了会增加生成代码大小的以下一些开关,
-falign-functions -falign-jumps
-falign-labels -falign-loops
-fprefetch-loop-arrays -freorder-blocks-algorithm=stc

本机**gcc version 7.4.0 (ubuntu1~18.04.1)**环境下,-Os相对于-O2只开启了优化开关:

1
-finline-functions          		[enabled]

且相对于-O2关闭了优化:

1
-foptimize-strlen           		[enabled]

-Ofast

为了提高程序的执行速度,GCC可以无视严格的语言标准。-Ofast会开启所有-O3的编译开关,且会对不符合标准的程序进行优化。

-Og

优化调试信息。相对于-O0生成的调试信息,-Og是为了能够生成更好的调试信息。和-O0一样,-Og选项关闭了很多优化开关。

如果同时使用多个不同level -O优化选项来进行编译,编译器会根据最后一个-O的level来决定采用那种优化级别。

GCC中与机器无关的编译选项的格式都是-fflag,flag为对应的编译选项,许多编译选项都有两个:开启关闭,开启和关闭通过-no前缀来区分:

  • 关闭:-fno-flag;
  • 开启:-fflag;

在开启-O多个level的编译优化时候,有可能会根据具体需要,对应的默认编译选项可能需要调整,在-O后面加上对应编译选项开关就可以了,gcc对于多个冲突的编译选项会以最后的为准

2. GCC 调试选项

GCC允许编译时添加额外的调试信息,以便程序进行调试,大部分情况下,你需要编译选项-g就可以满足调试需求。

GCC允许您将-g与-O配合使用。GCC开启优化编译选项的结果有时可能会令人惊讶:

  • 声明的某些变量可能被删除;

  • 控制流走到您意想不到的位置;

  • 有些语句可能不会执行,因为它们计算的是常量结果或它们的值已经在手边;

  • 有些语句可能会在不同的位置执行,因为它们已经移出了循环。

基于上述原因,可调试的优化输出,对于可能有bug的程序是很有必要的。

如果您没有使用其他优化选项,请考虑将-Og与-g一起使用。在完全没有-O选项的情况下,一些编译器收集对调试有用的信息根本不会运行,因此-Og可能会带来更好的调试体验。

-g

生成操作系统原生格式(stabs, COFF, XCOFF, or DWARF)的调试信息,gdb可以使用这些调试信息。在使用stabs调试格式的系统中,-g会为gdb调试工具生成额外的调试信息。

  • stabssymbol table strings,一种调试数据格式,创建于1980s,由于当时Unix系统最流行的目标文件格式a.out并没有规定如果存储调试信息,stabs通过将调试信息编码到目标文件的符号表中来进行调试。这在Unix操作系统中流行了很长一段时间,现在已经被更流行DWARF格式所取代。
  • COFFCommon Object File Format ,是Unix操作系统上的一种通用目标文件格式,是为可执行文件,目标文件,共享库所定义的一种格式。在Unix System V中引入,以取代之前的a.out目标文件格式,随着发展生成了XCOFF和ECOFF等扩展。现在已经被ELF格式所取代。不过COFF以及相关扩展格式仍然在Unix-like系统,Microsoft Windows和一些嵌入式系统中使用。
  • XCOFFeXtended COFF,是IBM基于COFF目标文件格式扩展的一种格式;在IBM机器中仍然采用;
  • DWARF: 目前广泛流行使用的标准化调试数据格式;DWARF最开始设计时是独立于ELF的,它同样是一种目标文件格式。根据wiki介绍,DWARF是伴随ELF开发的,由于ELF在中世纪文学中有精灵的意思,所以作者用”Dwarf”小矮人来命名这个项目,作者后来提出了Debugging With Attributed Record Formats的缩写。关于DWARF的实现可参考DWARF手册

-ggdb

生成GDB调试的调试信息

-gdwarf

生成DWARF格式的调试信息。

-gstabs

生成stabs格式的调试信息,不包含GDB扩展信息;

-glevel

和-O优化选项一样,生成不同级别的调试信息。

  • -g0:不生成调试信息,相当于没有使用-g;
  • -g1:生成最小的调试信息,足够在不打算调试的程序中进行堆栈查看。最小调试信息包括函数描述,外部变量,行数表,但不包括局部变量信息。
  • -g2:默认-g的调试级别;
  • -g3:相对-g,生成额外的信息,例如所有的宏定义;

和-O一样,如果多个级别的-g选项同时存在,最后的选项会被生效

下面简单的看一下在开启调试信息的编译中,生成的目标文件会有什么不一样。以下面代码为例:

1
2
3
4
5
6
#include <iostream>

const char * a = "walkerdu";
int main() {
const char * b = "lss";
}

在加上-g选项后,目标文件会新增很多调试段信息,如下:

1
2
3
4
5
6
7
8
9
10
[26] .debug_aranges    PROGBITS         0000000000000000  00001043
0000000000000030 0000000000000000 0 0 1
[27] .debug_info PROGBITS 0000000000000000 00001073
000000000000288b 0000000000000000 0 0 1
[28] .debug_abbrev PROGBITS 0000000000000000 000038fe
00000000000005ad 0000000000000000 0 0 1
[29] .debug_line PROGBITS 0000000000000000 00003eab
00000000000003d1 0000000000000000 0 0 1
[30] .debug_str PROGBITS 0000000000000000 0000427c
00000000000019d7 0000000000000001 MS 0 0 1

且-g生成的可执行文件是不带-g的3倍。项目代码在-O2下生成的ELF文件大小是116MB,而在-O2 -g编译选项下竟然有2.3GB,是不是很恐怖。

综上,很多项目的线上版本都是使用”-O2 -g”的编译选项进行编译,以便发生问题的时候容易定位。但这有一个很大的弊端就是目标文件会比不开启调试信息的情况下大很多,所以一般对外发布的软件都是不含有调试信息的release版本,同时也会发布含有调试信息的debug版本,两者的性能是一样的只是debug多了调试信息而已。

3. 参考

Optimize-Options

Debugging-Options