linux 代码优化措施

gcc提供了大量优化选项,用来对编译时间,目标文件长度,执行效率三个维度进行不同的取舍和平衡。

gcc 常用编译选项
-c 只编译并生成目标文件。
-E 只运行 C 预编译器。
-g 生成调试信息。GNU 调试器可利用该信息。
-Os 相对语-O2.5。
-o FILE 生成指定的输出文件。用在生成可执行文件时。
-O0 不进行优化处理。
-O 或 -O1 优化生成代码。
-O2 进一步优化。
-O3 比 -O2 更进一步优化,包括 inline 函数。
-shared 生成共享目标文件。通常用在建立共享库时。
-W 开启所有 gcc 能提供的警告。
-w 不生成任何警告信息。
-Wall 生成所有警告信息。
我们对-O0 -O1 -O2 -O3 四级优化选项及每级分别做什么优化作下详细说明

-O0
-O0: 不做任何优化,这是默认的编译选项。

-O1
-O1:优化会消耗少多的编译时间,它主要对代码的分支,常量以及表达式等进行优化。

-O和-O1: 对程序做部分编译优化,对于大函数,优化编译占用稍微多的时间和相当大的内存。使用本项优化,编译器会尝试减小生成代码的尺寸,以及缩短执行时间,但并不执行需要占用大量编译时间的优化。 -O1打开的优化选项:

l -fdefer-pop:延迟栈的弹出时间。当完成一个函数调用,参数并不马上从栈中弹出,而是在多个函数被调用后,一次性弹出。
l -fmerge-constants:尝试横跨编译单元合并同样的常量(string constants and floating point constants)
l -fthread-jumps:如果某个跳转分支的目的地存在另一个条件比较,而且该条件比较包含在前一个比较语句之内,那么执行本项优化.根据条件是true或者false,前面那条分支重定向到第二条分支的目的地或者紧跟在第二条分支后面.
l -floop-optimize:执行循环优化,将常量表达式从循环中移除,简化判断循环的条件,并且optionally do strength-reduction,或者将循环打开等。在大型复杂的循环中,这种优化比较显著。
l -fif-conversion:尝试将条件跳转转换为等价的无分支型式。优化实现方式包括条件移动,min,max,设置标志,以及abs指令,以及一些算术技巧等。
l -fif-conversion2基本意义相同,没有找到更多的解释。
l -fdelayed-branch:这种技术试图根据指令周期时间重新安排指令。 它还试图把尽可能多的指令移动到条件分支前, 以便最充分的利用处理器的治理缓存。
l -fguess-branch-probability:当没有可用的profiling feedback或__builtin_expect时,编译器采用随机模式猜测分支被执行的可能性,并移动对应汇编代码的位置,这有可能导致不同的编译器会编译出迥然不同的目标代码。
l -fcprop-registers:因为在函数中把寄存器分配给变量, 所以编译器执行第二次检查以便减少调度依赖性(两个段要求使用相同的寄存器)并且删除不必要的寄存器复制操作。
-O2
-O2:会尝试更多的寄存器级的优化以及指令级的优化,它会在编译期间占用更多的内存和编译时间。
Gcc将执行几乎所有的不包含时间和空间折中的优化。当设置O2选项时,编译器并不进行循环打开()loop unrolling以及函数内联。与O1比较而言,O2优化增加了编译时间的基础上,提高了生成代码的执行效率。 O2打开所有的O1选项,并打开以下选项:

l -fforce-mem:在做算术操作前,强制将内存数据copy到寄存器中以后再执行。这会使所有的内存引用潜在的共同表达式,进而产出更高效的代码,当没有共同的子表达式时,指令合并将排出个别的寄存器载入。这种优化对于只涉及单一指令的变量, 这样也许不会有很大的优化效果. 但是对于再很多指令(必须数学操作)中都涉及到的变量来说, 这会时很显著的优化, 因为和访问内存中的值相比 ,处理器访问寄存器中的值要快的多。
l -foptimize-sibling-calls:优化相关的以及末尾递归的调用。通常, 递归的函数调用可以被展开为一系列一般的指令, 而不是使用分支。 这样处理器的指令缓存能够加载展开的指令并且处理他们, 和指令保持为需要分支操作的单独函数调用相比, 这样更快。
l -fstrength-reduce:这种优化技术对循环执行优化并且删除迭代变量。 迭代变量是捆绑到循环计数器的变量, 比如使用变量, 然后使用循环计数器变量执行数学操作的for-next循环。
l -fcse-follow-jumps:在公用子表达式消元时,当目标跳转不会被其他路径可达,则扫描整个的跳转表达式。例如,当公用子表达式消元时遇到if...else...语句时,当条为false时,那么公用子表达式消元会跟随着跳转。
l -fcse-skip-blocks:与-fcse-follow-jumps类似,不同的是,根据特定条件,跟随着cse跳转的会是整个的blocks
l -frerun-cse-after-loop:在循环优化完成后,重新进行公用子表达式消元操作。
l -frerun-loop-opt:两次运行循环优化 l -fgcse:执行全局公用子表达式消除pass。这个pass还执行全局常量和copy propagation。这些优化操作试图分析生成的汇编语言代码并且结合通用片段, 消除冗余的代码段。如果代码使用计算性的goto, gcc指令推荐使用-fno-gcse选项。
l-fgcse-lm:全局公用子表达式消除将试图移动那些仅仅被自身存储kill的装载操作的位置。这将允许将循环内的load/store操作序列中的load转移到循环的外面(只需要装载一次),而在循环内改变成copy/store序列。在选中-fgcse后,默认打开。
l -fgcse-sm:当一个存储操作pass在一个全局公用子表达式消除的后面,这个pass将试图将store操作转移到循环外面去。如果与-fgcse-lm配合使用,那么load/store操作将会转变为在循环前load,在循环后store,从而提高运行效率,减少不必要的操作。
l -fgcse-las:全局公用子表达式消除pass将消除在store后面的不必要的load操作,这些load与store通常是同一块存储单元(全部或局部)
l-fdelete-null-pointer-checks:通过对全局数据流的分析,识别并排出无用的对空指针的检查。编译器假设间接引用空指针将停止程序。 如果在间接引用之后检查指针,它就不可能为空。
l -fexpensive-optimizations:进行一些从编译的角度来说代价高昂的优化(这种优化据说对于程序执行未必有很大的好处,甚至有可能降低执行效率,具体不是很清楚)
l -fregmove:编译器试图重新分配move指令或者其他类似操作数等简单指令的寄存器数目,以便最大化的捆绑寄存器的数目。这种优化尤其对双操作数指令的机器帮助较大。
l -fschedule-insns:编译器尝试重新排列指令,用以消除由于等待未准备好的数据而产生的延迟。这种优化将对慢浮点运算的机器以及需要load memory的指令的执行有所帮助,因为此时允许其他指令执行,直到load memory的指令完成,或浮点运算的指令再次需要cpu。 l
-fschedule-insns2:与-fschedule-insns相似。但是当寄存器分配完成后,会请求一个附加的指令计划pass。这种优化对寄存器较小,并且load memory操作时间大于一个时钟周期的机器有非常好的效果。
l -fsched-interblock:这种技术使编译器能够跨越指令块调度指令。 这可以非常灵活地移动指令以便等待期间完成的工作最大化。
l -fsched-spec-load:允许一些load指令进行一些投机性的动作。(具体不详)相同功能的还有-fsched-spec-load-dangerous,允许更多的load指令进行投机性操作。这两个选项在选中-fschedule-insns时默认打开。
l -fcaller-saves:通过存储和恢复call调用周围寄存器的方式,使被call调用的value可以被分配给寄存器,这种只会在看上去能产生更好的代码的时候才被使用。(如果调用多个函数, 这样能够节省时间, 因为只进行一次寄存器的保存和恢复操作, 而不是在每个函数调用中都进行。)
l -fpeephole2:允许计算机进行特定的观察孔优化(这个不晓得是什么意思),-fpeephole与-fpeephole2的差别在于不同的编译器采用不同的方式,由的采用-fpeephole,有的采用-fpeephole2,也有两种都采用的。
l -freorder-blocks:在编译函数的时候重新安排基本的块,目的在于减少分支的个数,提高代码的局部性。
l -freorder-functions:在编译函数的时候重新安排基本的块,目的在于减少分支的个数,提高代码的局部性。这种优化的实施依赖特定的已存在的信息:.text.hot用于告知访问频率较高的函数,.text.unlikely用于告知基本不被执行的函数。
l -fstrict-aliasing:这种技术强制实行高级语言的严格变量规则。 对于c和c++程序来说, 它确保不在数据类型之间共享变量. 例如, 整数变量不和单精度浮点变量使用相同的内存位置。
l -funit-at-a-time:在代码生成前,先分析整个的汇编语言代码。这将使一些额外的优化得以执行,但是在编译器间需要消耗大量的内存。(有资料介绍说:这使编译器可以重新安排不消耗大量时间的代码以便优化指令缓存。)
l -falign-functions:这个选项用于使函数对准内存中特定边界的开始位置。 大多数处理器按照页面读取内存,并且确保全部函数代码位于单一内存页面内, 就不需要叫化代码所需的页面。
l -falign-jumps:对齐分支代码到2的n次方边界。在这种情况下,无需执行傀儡指令(dummy operations)
l -falign-loops:对齐循环到2的n次幂边界。期望可以对循环执行多次,用以补偿运行dummy operations所花费的时间。
l -falign-labels:对齐分支到2的n次幂边界。这种选项容易使代码速度变慢,原因是需要插入一些dummy operations当分支抵达usual flow of the code.
l -fcrossjumping:这是对跨越跳转的转换代码处理, 以便组合分散在程序各处的相同代码。 这样可以减少代码的长度, 但是也许不会对程序性能有直接影响。
-O3
-O3: 在O2的基础上进行更多的优化。例如使用伪寄存器网络,普通函数的内联,以及针对循环的更多优化。在包含了O2所有的优化的基础上,又打开了以下优化选项:

l -finline-functions:内联简单的函数到被调用函数中。
l -fweb:构建用于保存变量的伪寄存器网络。 伪寄存器包含数据, 就像他们是寄存器一样, 但是可以使用各种其他优化技术进行优化, 比如cse和loop优化技术。这种优化会使得调试变得更加的不可能,因为变量不再存放于原本的寄存器中。
l -frename-registers:在寄存器分配后,通过使用registers left over来避免预定代码中的虚假依赖。这会使调试变得非常困难,因为变量不再存放于原本的寄存器中了。
l -funswitch-loops:将无变化的条件分支移出循环,取而代之的将结果副本放入循环中。
-Os
-Os:相当于-O2.5。是使用了所有-O2的优化选项,但又不缩减代码尺寸的方法。

 

编译选项和内核编译

首先我们都知道,Linux内核如果用O0编译,是无法编译过的,Linux的内核编译,要么是O2,要么是Os,这点从Linux的Makefile里面可以看出:

 

当选择了

CONFIG_CC_OPTIMIZE_FOR_SIZE

它会是Os,否则就是O2。

 

其实O2和Os,都是一些优化选项的集合:

gcc -c -Q -O2 --help=optimizers > /tmp/O2-opts

gcc -c -Q -Os --help=optimizers > /tmp/Os-opts

前者倾向于基于速度的优化,后者倾向于基于size更小的优化。对比二者的开关选项:

meld /tmp/O2-opts /tmp/Os-opts

发现差异小的可怜:

 

 

 

O2和Os都使能了inline small函数和called once的函数,但是O2里面-finline-functions是关闭的,而Os里面是开的。O2里面optimize-strlen是开的,Os里面这个选项是关闭的。相关选项的含义可以通过"man gcc"看出(有问题,找男人),譬如man gcc后检索inline-functions:

 

 

从O0到O1,O2,O3,是一个开启的优化选项逐步加大的过程:

 

kernel用O0编译不过,是因为kernel本身也没有想用O0能够编译过,它的设计里面包含了编译会优化的假想。下面我们用一个简单的例子来说明。

02

一个简单的例子

下面的代码:

 

 

O0编译会报如下错,说f()函数没有定义:

$ gcc -O0 cc.c

cc.c:1:13: warning: ‘f’ used but never defined [enabled by default]

void f(void);

^

/tmp/ccTwwtHG.o: In function `main':

cc.c:(.text+0x19): undefined reference to `f'

collect2: error: ld returned 1 exit status

但是用O2编译,则没有问题:

$ gcc -O2 cc.c

原因在于,O2编译,它意识到a==1,所以if(a>2),它不会成立,所以f()没有定义也没有关系。

把代码稍微改一下后:

 

O2这个时候也不行了:

$ gcc -O2 cc.c

/tmp/ccXiyBHn.o: In function `main':

cc.c:(.text.startup+0x7): undefined reference to `f'

collect2: error: ld returned 1 exit status

所以,通过这个例子,大家可以看出来为什么同样的代码,用O2就可以过,用O0就过不了。内核里面有许多类似设想编译器会进行优化的代码。

3.我们不想inline了

由于编译的优化,有些函数(比如小函数和全工程里面只被一个人调用的函数)虽然没有显示地写成inline,但是编译器优化为inline了,这给调试造成了一些麻烦,因为找不到这个函数对应的symbol了。

 

这个时候,我们可以显示地写明某些函数我们不想inline:

 

否则,上面2个函数,即便你代码里面没有写inline,由于O2和Os使能了相关的inline选项,也可能被编译器自动inline掉,如果我们想拒绝inline,可以通过noline来标识。

4.我不想被优化

在全局已经使能O1, O2, O3, Os的情况下,某个单独的函数我们不想做任何的优化,可以用__attribute__((optimize("O0")))来修饰这个函数,比如我们把上述用O2可以编译过的代码进行如下修改:

 

 

重新用O2编译:

$ gcc -O2 cc.c

/tmp/cc8M338p.o: In function `main':

cc.c:(.text+0x19): undefined reference to `f'

collect2: error: ld returned 1 exit status

5. 总结的话

下面给几条实践指南:

尽量不要尝试用O0去编译内核,这不符合真实的工程实践,也不太被主流Linux社区所支持;内核依赖O2/Os去做较多的优化;

追求你的代码在O2的情况下,仍然是正确的,代码要经得起编译优化;比如O0工作正常,而O2不正常,应该尽可能从自身找原因,分析汇编;

如果在全局优化的情况下,想针对某个局部避免优化,可以尝试用noinline,__attribute__((optimize("O0")))等进行外科手术式地调整。

 

posted @ 2022-05-03 09:11  liujunhuasd  阅读(339)  评论(0编辑  收藏  举报