分析過程,價值不大
一、什麼是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
。某個函數可能太大,不能直接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
,它們是什麼意思
參考
使用的工具,文檔列表。
- 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