> 文章列表 > GCC编译优化选项及汇编优化实例

GCC编译优化选项及汇编优化实例

GCC编译优化选项及汇编优化实例

文章目录

  • 1 优化级别
    • -O1
    • -O2
    • -O3
    • -O0
    • -Os
    • -Ofast
    • -Og
    • -Oz
    • -On (n>=4)?
  • 2 汇编优化实例

最近在做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代码为例,该代码实现s2s1的拷贝,最后返回s1地址

char *cpy(char *s1, const char *s2)
{char *ret= s1;while((*s1++ = *s2++));return ret;
}

-O0汇编

cpy:# 这里需要用到rbp寄存器,但是它里面的值可能caller还需要使用,将其压栈push    rbp# 使用栈rsp之后的内存来保存s1和s2mov     rbp, rsp# 即*(rbp-24) = s1,*(rbp-32) = s2mov     QWORD PTR [rbp-24], rdimov     QWORD PTR [rbp-32], rsi# rax = s1的地址mov     rax, QWORD PTR [rbp-24]# 即*(rbp-8) = rax = s1mov     QWORD PTR [rbp-8], rax# 指令流水线对齐?nop
.L2:# 即rdx = s2mov     rdx, QWORD PTR [rbp-32]# 即rax = ++s2,即取s2的下一个值的地址lea     rax, [rdx+1]# 即QWORD PTR [rbp-32] = ++s2,为下一次赋值做准备mov     QWORD PTR [rbp-32], rax# 即rax = s1mov     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跳出循环,否则继续回去执行L2test    al, aljne     .L2# 返回值保存在rax,即s1的地址mov     rax, QWORD PTR [rbp-8]# rbp出栈,恢复函数调用前的状态pop     rbpret

-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跳出循环,否则继续回去执行L2test    cl, cljne     .L2ret

可以看出,如果编译器优化没有打开,代码执行的效率是比较低的,两个字符串指针各自计数,也没有必要用到栈和那么多寄存器。由于程序很简单,在本例中的-O1-O2-O3-Ofast的优化结果是一样的,当然我们需要根据实际情况更改优化等级,有时调试出现奇怪的、未知的结果可能就是编译优化导致的。