記一次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<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 dumpWhat 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章