最近在做LVGL的GUI,移植官方的SDK,然后CPU占用率竟然达到了99%,这就让整个GUI界面的反应十分缓慢。然后我开启了编译器的优化
-O3
,CPU的使用率降低到了50%,今天来简单介绍一下GCC的编译优化级别。
编译器优化级别由命令行选项-On
控制,其中n
是所需的优化级别。打开优化标志会使编译器提高性能和优化代码大小,但会牺牲编译时间和部分语句的Debug功能。
1 优化级别
-O1
对一些大型函数,优化编译时间和程序大小。-O1
等价于使用下面几个编译器flags:
-fauto-inc-dec
-fbranch-count-reg
-fcombine-stack-adjustments
-fcompare-elim
-fcprop-registers
-fdce
-fdefer-pop
-fdelayed-branch
-fdse
-fforward-propagate
-fguess-branch-probability
-fif-conversion
-fif-conversion2
-finline-functions-called-once
-fipa-modref
-fipa-profile
-fipa-pure-const
-fipa-reference
-fipa-reference-addressable
-fmerge-constants
-fmove-loop-invariants
-fmove-loop-stores
-fomit-frame-pointer
-freorder-blocks
-fshrink-wrap
-fshrink-wrap-separate
-fsplit-wide-types
-fssa-backprop
-fssa-phiopt
-ftree-bit-ccp
-ftree-ccp
-ftree-ch
-ftree-coalesce-vars
-ftree-copy-prop
-ftree-dce
-ftree-dominator-opts
-ftree-dse
-ftree-forwprop
-ftree-fre
-ftree-phiprop
-ftree-pta
-ftree-scev-cprop
-ftree-sink
-ftree-slsr
-ftree-sra
-ftree-ter
-funit-at-a-time
-O2
编译器执行的优化不需要为了速度而牺牲空间。与-O1
相比,-O2
优化提高了输出二进制文件的性能,但也增加了编译时间。除了-O1
打开的编译器flag外,它还会打开
-falign-functions -falign-jumps
-falign-labels -falign-loops
-fcaller-saves
-fcode-hoisting
-fcrossjumping
-fcse-follow-jumps -fcse-skip-blocks
-fdelete-null-pointer-checks
-fdevirtualize -fdevirtualize-speculatively
-fexpensive-optimizations
-ffinite-loops
-fgcse -fgcse-lm
-fhoist-adjacent-loads
-finline-functions
-finline-small-functions
-findirect-inlining
-fipa-bit-cp -fipa-cp -fipa-icf
-fipa-ra -fipa-sra -fipa-vrp
-fisolate-erroneous-paths-dereference
-flra-remat
-foptimize-sibling-calls
-foptimize-strlen
-fpartial-inlining
-fpeephole2
-freorder-blocks-algorithm=stc
-freorder-blocks-and-partition -freorder-functions
-frerun-cse-after-loop
-fschedule-insns -fschedule-insns2
-fsched-interblock -fsched-spec
-fstore-merging
-fstrict-aliasing
-fthread-jumps
-ftree-builtin-call-dce
-ftree-loop-vectorize
-ftree-pre
-ftree-slp-vectorize
-ftree-switch-conversion -ftree-tail-merge
-ftree-vrp
-fvect-cost-model=very-cheap
-O3
在-O2
的基础上,还需要在空间和速度上进行权衡,它还会打开:
-fgcse-after-reload
-fipa-cp-clone
-floop-interchange
-floop-unroll-and-jam
-fpeel-loops
-fpredictive-commoning
-fsplit-loops
-fsplit-paths
-ftree-loop-distribution
-ftree-partial-pre
-funswitch-loops
-fvect-cost-model=dynamic
-fversion-loops-for-strides
-O0
默认值,它将减少编译时间,调试将产生预期的结果。启用了部分flags,如-falign-loops
, -finline-functions-called-once
,和-fmove-loop-invariants
-Os
减少二进制文件的大小,而不是执行速度。-Os
启用所有-O2
中的flags,但除去了以下会增加代码大小的flags:
-falign-functions -falign-jumps
-falign-labels -falign-loops
-fprefetch-loop-arrays -freorder-blocks-algorithm=stc
-Ofast
无视严格的标准遵守。-Ofast
启用所有-O3
优化。它还支持对所有符合标准的程序无效的优化。除非指定了-fmax-stack-var-size
和-fno-protect-parens
,否则它会开启-fast-math
, -flow -store-data-races
和Fortran专用的-fstack-arrays
。它关闭了-fsemantics -interposition
。
-Og
优化调试体验,它只是禁用可能干扰调试的优化。-Og
提供合理的优化,同时保证良好的调试体验。对于生成可调试代码来说,它是比-O0
更好的选择,因为一些收集调试信息的编译器通道在-O0
处被禁用。-Og
在-O1
的基础上还启用了以下几个编译器flags:
-fbranch-count-reg -fdelayed-branch
-fdse -fif-conversion -fif-conversion2
-finline-functions-called-once
-fmove-loop-invariants -fmove-loop-stores -fssa-phiopt
-ftree-bit-ccp -ftree-dse -ftree-pta -ftree-sra
-Oz
在GCC 12.1中引入,在-Os
的基础上进一步优化程序大小。注意,相比-O2
,它会严重降低运行时性能,因为如果这些指令需要更少的字节来编码,则会增加执行的指令数量。-Oz
的行为类似于-Os
,包括启用大多数-O2
中的优化。
-On (n>=4)?
-O
级别高于3没有任何效果。编译器可能会接受像-O4
这样的cflag,但实际上它不会对它们做任何事情,它只对-O3
进行优化。
- 参考文章(内含各个flags的说明):https://gcc.gnu.org/onlinedocs/gcc/Optimize-Options.html
2 汇编优化实例
- 编译器:
x86-64 gcc 12.2
以下面的C代码为例,该代码实现s2
到s1
的拷贝,最后返回s1
的地址。
char *cpy(char *s1, const char *s2)
{
char *ret= s1;
while((*s1++ = *s2++));
return ret;
}
-O0
汇编
cpy:
# 这里需要用到rbp寄存器,但是它里面的值可能caller还需要使用,将其压栈
push rbp
# 使用栈rsp之后的内存来保存s1和s2
mov rbp, rsp
# 即*(rbp-24) = s1,*(rbp-32) = s2
mov QWORD PTR [rbp-24], rdi
mov QWORD PTR [rbp-32], rsi
# rax = s1的地址
mov rax, QWORD PTR [rbp-24]
# 即*(rbp-8) = rax = s1
mov QWORD PTR [rbp-8], rax
# 指令流水线对齐?
nop
.L2:
# 即rdx = s2
mov rdx, QWORD PTR [rbp-32]
# 即rax = ++s2,即取s2的下一个值的地址
lea rax, [rdx+1]
# 即QWORD PTR [rbp-32] = ++s2,为下一次赋值做准备
mov QWORD PTR [rbp-32], rax
# 即rax = s1
mov rax, QWORD PTR [rbp-24]
# 即rcx = ++s1,即取s1的下一个值的地址
lea rcx, [rax+1]
# 即QWORD PTR [rbp-24] = ++s1,为下一次赋值做准备
mov QWORD PTR [rbp-24], rcx
# 取s2中的一个字节到edx寄存器中
movzx edx, BYTE PTR [rdx]
# 即将*s2的一个字节拷贝到*s1中
mov BYTE PTR [rax], dl
# 最后取本次赋的值到eax,其中eax由低位al和高位ah组成,可以直接使用这两个寄存器标号
movzx eax, BYTE PTR [rax]
# 逻辑与操作,当且仅当拷贝到最后的结束符\0(ASCII码为0)时,jne跳出循环,否则继续回去执行L2
test al, al
jne .L2
# 返回值保存在rax,即s1的地址
mov rax, QWORD PTR [rbp-8]
# rbp出栈,恢复函数调用前的状态
pop rbp
ret
-O1
汇编
cpy:
# s1的地址保存在rdi寄存器中,赋值给rax寄存器
mov rax, rdi
# 初始化edx寄存器
mov edx, 0
.L2:
# 读取(s2+偏移rdx)地址中的内容
movzx ecx, BYTE PTR [rsi+rdx]
# cl即ecx的低位,即*(s1+rdx)=*(s2+rdx)
mov BYTE PTR [rax+rdx], cl
# 计数器增加
add rdx, 1
# 逻辑与操作,当且仅当拷贝到最后的结束符\0(ASCII码为0)时,jne跳出循环,否则继续回去执行L2
test cl, cl
jne .L2
ret
可以看出,如果编译器优化没有打开,代码执行的效率是比较低的,两个字符串指针各自计数,也没有必要用到栈和那么多寄存器。由于程序很简单,在本例中的-O1
与-O2
、-O3
和-Ofast
的优化结果是一样的,当然我们需要根据实际情况更改优化等级,有时调试出现奇怪的、未知的结果可能就是编译优化导致的。