记一次gcc -O2大幅度增加binary size的问题

分析过程,价值不大

一、什么是O2

在编译folly的过程中,添加了-O2选项,libfolly.a的binary size从184M增加到了273M,为什么folly会增加这么多?

# default optionls -al -h | grep libfolly.a
-rw-r--r-- 1 wangliushuai wangliushuai 184M May 21 14:00 libfolly.a
# add -O2ls -al -h | grep libfolly.a
-rw-r--r-- 1 wangliushuai wangliushuai 273M May 21 11:57 libfolly.a

如果不指定任何优化选项,gcc默认是-O0,虽然是-O0,但也并不是什么优化都不做,一些简单的常量传播或者公共子表达式消除还是可以实现的。-O2会打开-O1的选项,并提供如下选项:

// The optimization flags of O1
-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-profile 
-fipa-pure-const 
-fipa-reference 
-fipa-reference-addressable 
-fmerge-constants 
-fmove-loop-invariants 
-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
// The optimization flags of O2
-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-pre 
-ftree-switch-conversion  -ftree-tail-merge 
-ftree-vrp

可以看到-O2开启了很多optimization flags,哪一个才是导致binary size增加这么多原因呢?

二、追查过程

首先就是通过libfolly.a本身入手,对比加-O2和不加的两者的区别,此时使用的工具是size。由于libfolly.a是archive,所以挑选其中一个典型的Future.cpp.o查看,对于-O2版本来讲,size命令显示出来的结果和ls命令显示出来的结果是相违背的。

# default option
▶ ls -al -h | grep Future.cpp.o
-rw-r--r-- 1 wangliushuai wangliushuai   19M May 21 14:08 Future.cpp.o

▶ size Future.cpp.o
   text           data            bss            dec            hex        filename
1203973           5632            177        1209782         1275b6        Future.cpp.o

# -O2
▶ ls -al -h | grep Future.cpp.o
-rw-r--r-- 1 wangliushuai wangliushuai  31M May 21 14:07 Future.cpp.o

▶ size Future.cpp.o
   text           data            bss            dec            hex        filename
 586635           4072            177         590884          90424        Future.cpp.o

ls命令得到的结果肯定是准确的,所以问题肯定处在了size命令上,查询size工具的实现原理参见https://stackoverflow.com/a/31238689/10481594,对于text列的结果是如下三个section之后(section是linker角度来看,而segment是从os运行角度来看的):

  • .text
  • .rodata
  • .eh_frame

注:上图来源于http://www.skyfree.org/linux/references/ELF_Format.pdf

使用命令readelf -WS来查看object file中详细的section信息,发现有成千上万个section,但是通常的section不应该是几十个吗?像是.bss.coment.text.got等等。发现很多section name是.text.mangledname的形式,这就要介绍function-level linking的概念,linker在链接时会重定位并合并相同的段,例如.text。但是有可能一个.o中有10函数,但是只被使用了一个,链接时还是会把其余的9个无用的函数链接进去。而function-level linking,则把每个函数单独分配一个section,这样的话,只有被用到的section最终会被链接进去,而对于gcc来说,可以使用-ffunction-sections来enable这个机制,然后linker使用-Wl,–gc-sections来删除无用的代码。检查folly的build文件,发现有-ffunction-sections的。

3220   [3215] .text._ZN5folly6FutureIlE6getTryEv PROGBITS        0000000000000000 022e10 000079 00 AXG  0   0 16
3221   [3216] .rela.text._ZN5folly6FutureIlE6getTryEv RELA            0000000000000000 ecf910 0000c0 18  IG 5389 3215  8
3222   [3217] .text._ZN5folly6FutureINS_4UnitEE6getTryEv PROGBITS        0000000000000000 022e90 000079 00 AXG  0   0 16
3223   [3218] .rela.text._ZN5folly6FutureINS_4UnitEE6getTryEv RELA            0000000000000000 ecf9d0 0000c0 18  IG 5389 3217  8

所以size命令无法看出两者的差别,所以只能从section header入手查看区别,首先-O2 option的function-section数量是原来的1/5。

# default option
▶ readelf -WS Future.cpp.o| grep ".text.*" |  wc -l
16210
# add -O2 option
▶ readelf -WS Future.cpp.o| grep ".text.*" |  wc -l
3260

然后随便挑选一个function section查看,我们可以看到函数
folly::exception_wrapper::InPlace::delete_(folly::exception_wrapper*)对应function section的size从0x35->0x16

# default option
[18348] .text._ZN5folly17exception_wrapper7InPlaceINS_18FutureNoTimekeeperEE7delete_EPS0_ PROGBITS        0000000000000000 0a5dea 000035 00 AXG  0   0  2
[18349] .rela.text._ZN5folly17exception_wrapper7InPlaceINS_18FutureNoTimekeeperEE7delete_EPS0_ RELA            0000000000000000 bf5df8 000030 18  IG 25869 18348  8

# add -O2 option
[1813] .text._ZN5folly17exception_wrapper7InPlaceINS_18FutureNoTimekeeperEE7delete_EPS0_ PROGBITS        0000000000000000 006720 000016 00 AXG  0   0 16
[1814] .rela.text._ZN5folly17exception_wrapper7InPlaceINS_18FutureNoTimekeeperEE7delete_EPS0_ RELA            0000000000000000 eba9e8 000018 18  IG 5389 1813  8

function section数量减小 && 某些function section size增大,很容易就可以得到这次binary size的增大,可能是inline导致的。

在猜测可能是inline导致的问题之后,这里首先禁掉所有与inline相关的optimization flags。binary size从273M降低到了267M,也就是inline给binary size的增加带来了一定的影响,但并不是决定性的影响。

CXX_FLAGS="-O2" --> CXX_FLAGS="-fno-indirect-inlining -fno-partial-inlining -fno-inline-small-functions  -fno-inline-functions -fno-inline-functions-called-once -O2"
# default option
▶ readelf -WS Future.cpp.o| grep ".text.*" |  wc -l
16210
# add -O2 option
▶ readelf -WS Future.cpp.o| grep ".text.*" |  wc -l
3260
# add -O2 && -fno-indirect-inlining -fno-partial-inlining -fno-inline-small-functions  -fno-inline-functions -fno-inline-functions-called-once 
▶ readelf -WS Future.cpp.o| grep ".text.*" |  wc -l
4820

# add -O2 && -fno-indirect-inlining -fno-partial-inlining -fno-inline-small-functions  -fno-inline-functions -fno-inline-functions-called-once -fno-inline -fno-devirtualize-speculatively -fno-devirtualize
▶ readelf -WS Future.cpp.o| grep ".text.*" |  wc -l
14417

禁掉inline相关的flags,但是function sections的数量从3260增加到4820,但是离最初的16210还差了很多。所以应该还有其它的对binary size起决定性作用的flag。我后面显示加了-fno-inline-fno-devirtualize-speculatively以及-fno-devirtualize后者会基于类型信息,把virutal call转变为direct call,从而enable更多的inline),binary size继续从267M降低到了209M,依次添加这个选项,binary size呈递减状态。

可见经过我不停地尝试,binary size逐渐下降,同时function sections的个数也逐渐回升。但是这样不停尝试过之后发现,gcc的optimization flags相互交叉,相互影响,比我预想的要复杂很多。遂放弃尝试,最后发现真正决定binary size的不是-O2中某个单一的flag,而是一组flag,但虽然不是某个flag起作用,但是终归还是和inline有关系

2.1 开启了-O2和-ffunction-sections的object file中.text section存放了什么内容?

但是最终还有一个小疑问,对于一个elf object file来说,使用-ffunction-sections时,所有函数都有对应的function section,为什么还有一个单独的.text段,它存储的是什么内容?

为了搞清楚这一点,使用下面的命令把Future.cpp.o中.text的内容打印出来。

objdump -dj .text Future.cpp.o

发现-O2 option的版本和不加-O2的版本的内容相差很多。对于添加了-O2 option的版本,有很多有.irsa.number.part.number.constprop.1521作为后缀的代码片段,所以:

  • irsa等代表了什么
  • 为什么这部分代码会放到了.text段,而不是单独的function sections段中。
<_ZNK5folly7futures6detail10FutureBaseISt5tupleIJNS_3TryIdEENS4_INS_4UnitEEEEEE16throwIfContinuedEv.isra.393>
// ...
<_ZNK5folly7futures6detail10FutureBaseISt5tupleIJNS_3TryIbEENS4_INS_4UnitEEEEEE16throwIfContinuedEv.isra.369>
// ...

_ZN5folly7futures6detail4CoreINS_4UnitEE13detachPromiseEv.part.618
// ...
_ZNSt14__shared_countILN9__gnu_cxx12_Lock_policyE2EEC2IN5folly6fibers5BatonESaIS6_EJEEERPT_St20_Sp_alloc_shared_tagIT0_EDpOT1_.constprop.1521

关于isra
stackoverflow上有一个相关的为问题What is “isra” in the kernel thread dump 和 What does the GCC function suffix “isra” mean?,其中提到了一个optimization flag -fipa-sra,这个flag是-O2 option添加的。

-fipa-sra
Perform interprocedural scalar replacement of aggregates, removal of unused parameters and replacement of parameters passed by reference by parameters passed by value.
Enabled at levels -O2, -O3 and -Os.

从字面意思来理解,这个优化做的事情是过程间的聚合类型的标量替换,把pass-by-reference替换为pass-by-value,翻译成中文有点儿绕口。

// 这里没有必要传递一个传递指针进去,然后再对指针进行解引用。
static int foo(int *m)
{
  return *m + 1;
}

int bar(void)
{
  int i = 1;
  return foo(&i);
}

// 其实可以优化成下面的样子,这种是中间态,我没有找到合适的option来切实得到下面的转换
static int foo(int m)
{
  return m + 1;
}

int bar(void)
{
  int i = 1;
  return foo(i);
}

注:上图示例来自于Interprocedural optimization in GCC

关于.part
关于part,stackoverflow上也有相关的问题C++ function name demangling: What does this name suffix mean?,这个也和-O2中的另外一个flag相关,也就是-fpartial-inlining。某个函数可能太大,不能直接inline,所以将函数的一部分进行inline,这一部分就单独拆分出来,通过mangled name加上.part后缀表示这个要被inline的子部分。

关于constprop
关于constprop,也有一个相关的问题What does the GCC function suffix .constprop mean?。可以看出来这个constprop,是和constant propagation相关的,从 https://github.com/gcc-mirror/gcc/blob/master/gcc/ipa-cp.c#L381 也得到了印证,虽然这个代码的细节我还没有时间看。

所以现在就可以回答“为什么在给定ffunction-sections的情况下,.text段还有如此之多code?”的问题了,因为添加了-O2选项,可能会对很多函数做优化,这些优化可能需要对函数进行某些变换,此时就需要在记录这些函数的“变化”版本。

三、结论

此次folly加-O2变大的主要原因是inline,但不是某一个inline flag导致的,而是多个优化flag综合在一起的作用。

四、学到的

  • gcc -O2 option会添加哪些优化flag
  • inline相关的option有哪些?
  • size的text值是怎么计算的
  • -ffunction-sections-Wl,--gc-sections是什么
  • .text段中有很多mangled name有.irsa.part.constprop,它们是什么意思

这里WWDC 2019的视频What’s New in Clang and LLVM在介绍-Oz时给出的一个图。另外一个相关的问题就是-O3真的很快吗?为什在不了解你的应用时,不要轻易使用-O3
记一次gcc -O2大幅度增加binary size的问题_第1张图片

注:上图来源于https://developer.apple.com/videos/play/wwdc2019/409/

参考
使用的工具,文档列表。

  • size
  • readelf
  • http://www.keil.com/support/man/docs/armclang_intro/armclang_intro_fnb1472741490155.htm
  • https://stackoverflow.com/questions/31227153/size-and-objdump-report-different-sizes-for-the-text-segment
  • https://www.gabriel.urdhr.fr/2015/09/28/elf-file-format/
  • https://linux-audit.com/elf-binaries-on-linux-understanding-and-analysis/
  • https://elinux.org/Function_sections
  • https://lwn.net/Articles/741494/
  • http://www.skyfree.org/linux/references/ELF_Format.pdf
  • https://static.lwn.net/images/conf/rtlws-2011/proc/Yong.pdf
  • https://kristerw.blogspot.com/2017/05/interprocedural-optimization-in-gcc.html
  • http://sciencewise.info/media/pdf/1010.2196v2.pdf
  • https://docs.google.com/presentation/u/1/d/1-K0ahFIAip12TJxAPJtQCpxZ4l6l19MneQMmKPQ-SjQ/htmlpresent
  • https://github.com/gcc-mirror/gcc/blob/master/gcc/ipa-cp.c#L381
  • https://stackoverflow.com/questions/14796686/what-does-the-gcc-function-suffix-constprop-mean
  • https://interrupt.memfault.com/blog/best-and-worst-gcc-clang-compiler-flags#the-best-and-worst-gcc-compiler-flags-for-embedded

你可能感兴趣的:(编译)