分析过程,价值不大
一、什么是O2
在编译folly的过程中,添加了-O2
选项,libfolly.a的binary size从184M增加到了273M,为什么folly会增加这么多?
# default option
▶ ls -al -h | grep libfolly.a
-rw-r--r-- 1 wangliushuai wangliushuai 184M May 21 14:00 libfolly.a
# add -O2
▶ ls -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<folly::FutureNoTimekeeper>::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
。某个函数可能太大 (partial inlining主要是为了解决hot/code code的问题,以最大限度利用L1I cache,避免frontend store),不能直接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
?
注:上图来源于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