[英]Which GCC optimization flags affect binary size the most?
我正在使用 GCC 為 ARM 開發一個 C++。我遇到了一個問題,我沒有啟用優化,我無法為我的代碼創建二進制文件 ( ELF ),因為它不適合可用空間。 但是,如果我簡單地啟用調試優化 ( -Og ),這是我所知的最低可用優化,代碼很容易適合。
在這兩種情況下,都啟用了-ffunction-sections 、 -fdata-sections 、 -fno-exceptions和 -Wl ,--gc-sections 。
即使進行最少的優化,這在二進制大小上也是巨大的差異。
我查看了3.11 控制優化的選項,了解有關使用 -Og 標志執行哪些優化的詳細信息,看看是否能給我任何見解。
哪些優化標志對二進制大小的影響最大? 有什么我應該尋找的來解釋這種巨大的差異嗎?
哪個 GCC 優化標志對二進制大小的影響最大?
它會因程序本身而有所不同。 找出每個標志如何影響您的程序的最准確方法是嘗試並將結果與基本級別進行比較。
大小優化的基本級別的一個很好的選擇是使用 -Os,它可以啟用 -O2 的所有優化,除了那些預計可能會顯着增加二進制大小的優化(目前):
-falign-functions
-falign-jumps
-falign-labels
-falign-loops
-fprefetch-loop-arrays
-freorder-blocks-algorithm=stc
大多數未優化構建的額外代碼大小是默認的-O0
也意味着調試構建,即使您使用 GDB j
命令跳轉到不同的語句,也不會在寄存器中保留任何內容以進行一致的調試function 中的源代碼行。-O0 -O0
大量的存儲/重新加載,即使是最輕的優化級別,對於不能使用 memory 源操作數的非 CISC ISA 上的代碼大小來說尤其是災難性的。 為什么 clang 使用 -O0 產生低效的 asm(對於這個簡單的浮點和)? 同樣適用於 GCC。
特別是對於現代 C++,調試構建是災難性的,因為簡單的模板包裝函數通常在簡單情況下(或可能是一條指令)內聯和優化為空,而是編譯為必須設置 args 並運行調用指令的實際 function 調用. 例如,對於std::vector
, operator[]
成員 function 通常可以內聯到單個ldr
指令,假設編譯器在寄存器中具有.data()
指針。 但是如果沒有內聯,每個調用點都會接受多條指令1
對實際.text
部分1中的代碼大小影響最大的選項:一般情況下分支目標的 alignment,或者只是循環,會花費一些代碼大小。 除此之外:
-ftree-vectorize
- 使 SIMD 版本循環,如果編譯器無法證明迭代計數將是向量寬度的倍數,則還需要進行標量清理。 (或者,如果您不使用restrict
,則指向 arrays 是非重疊的;這可能還需要標量回退)。 在 GCC11 及更早版本中在-O3
處啟用。 在 GCC12 及更高版本中在-O2
處啟用,例如 clang。
-funroll-loops
/ -funroll-all-loops
- 即使在現代 GCC 中的-O3
下,默認情況下也不啟用。啟用配置文件引導優化 ( -fprofile-use
),當它具有來自-fprofile-generate
構建的分析數據時知道哪些循環實際上很熱並且值得花費代碼大小。 (而且它們是冷的,因此應該針對大小進行優化,這樣當它們運行時你會得到更少的 I-cache 未命中,並且減少其他代碼的驅逐。)PGO 也影響矢量化決策。
與循環展開相關的是控制循環剝離(完全展開)和展開多少的試探法(調整旋鈕)。 設置這些的正常方法是使用-march=native
, 這意味着-mtune=
whatever as well 。 與-mtune=sandybridge
或-mtune=haswell
相比,-mtune -mtune=znver3
可能有利於較大的展開因子(至少 clang 有)。 但是有 GCC 選項可以手動調整個別事物,如gcc 的評論中所討論:為簡單循環生成奇怪的 asm和如何要求 GCC 完全展開此循環(即,剝離此循環)?
也有一些選項可以覆蓋其他決策啟發式的權重和閾值,例如內聯,但是您很少想要微調那么多,除非您正在努力改進默認值,或者為新 CPU 尋找好的默認值.
-Os
- 優化大小和速度,盡量不犧牲太多速度。 如果您的代碼有很多 I-cache 未命中,這是一個很好的權衡,否則-O3
通常更快,或者至少這是 GCC 的設計目標。值得嘗試不同的選項以查看-O2
或-Os
是否使您的代碼更快在您關心的某些 CPU 上比-O3
大; 有時某些微體系結構的錯誤優化或怪癖會產生影響,例如如果我針對大小而不是速度進行優化,為什么 GCC 會生成快 15-20% 的代碼? 對於測試程序中的特定小循環,它具有從 GCC4.6 到 4.8(當時的當前版本)的實際基准測試,在許多不同的 x86 和 ARM CPU 上,使用和不使用-march=native
來實際調整它們。 但是,沒有理由期望它能代表其他代碼,因此您需要針對自己的代碼庫進行自我測試。 (對於任何給定的循環,小的代碼更改可以使不同的編譯選項在任何給定的 CPU 上更好。)
顯然-Os
如果您需要更小的 static 代碼大小以適應某些大小限制,則非常有用。
-Oz
僅針對大小進行優化,即使在速度方面也有很大的代價。 GCC 最近才將它添加到當前的主干中,所以期待它在 GCC12 或 13 中。大概我在下面寫的關於 clang 的-Oz
實現非常激進的內容也適用於 GCC,但我還沒有測試它。
Clang 具有類似的選項,包括-Os
。 它還有一個clang -Oz
選項,只針對大小進行優化,而不關心速度。 它非常激進,例如在 x86 上使用代碼高爾夫技巧,如push 1; pop rax
push 1; pop rax
(總共 3 個字節)而不是mov eax, 1
(5 個字節)。
不幸的是,GCC 的-Os
選擇使用div
而不是乘法逆除法來除以常數,這會花費很多速度,但如果有任何大小,也不會節省太多。 (對於 x86-64,https://godbolt.org/z/x9h4vx1YG )。 對於 ARM,如果您不使用-mcpu=
,則 GCC -Os
仍然使用逆函數,這意味着udiv
甚至可用,否則它使用udiv
: https://godbolt.org/z/f4sa9Wqcj 。
Clang 的-Os
仍然使用與umull
的乘法逆,僅將udiv
與-Oz
一起使用。 (或調用__aeabi_uidiv
helper function 沒有任何-mcpu
選項)。 因此,在這方面, clang -Os
比 GCC 做出了更好的權衡,仍然花費了一點代碼大小來避免緩慢的 integer 除法。
std::vector
#include <vector>
int foo(std::vector<int> &v) {
return v[0] + v[1];
}
Godbolt與gcc
與默認-O0
與-Os
for -mcpu=cortex-m7
只是為了隨機選擇一些東西。 IDK 如果在實際的微控制器上使用像std::vector
這樣的動態容器是正常的; 可能不會。
# -Os (same as -Og for this case, actually, omitting the frame pointer for this leaf function)
foo(std::vector<int, std::allocator<int> >&):
ldr r3, [r0] @ load the _M_start member of the reference arg
ldrd r0, r3, [r3] @ load a pair of words (v[0..1]) from there into r0 and r3
add r0, r0, r3 @ add them into the return-value register
bx lr
與調試構建(為 asm 啟用名稱分解)
# GCC -O0 -mcpu=cortex-m7 -mthumb
foo(std::vector<int, std::allocator<int> >&):
push {r4, r7, lr} @ non-leaf function requires saving LR (the return address) as well as some call-preserved registers
sub sp, sp, #12
add r7, sp, #0 @ Use r7 as a frame pointer. -O0 defaults to -fno-omit-frame-pointer
str r0, [r7, #4] @ spill the incoming register arg to the stack
movs r1, #0 @ 2nd arg for operator[]
ldr r0, [r7, #4] @ reload the pointer to the control block as the first arg
bl std::vector<int, std::allocator<int> >::operator[](unsigned int)
mov r3, r0 @ useless copy, but hey we told GCC not to spend any time optimizing.
ldr r4, [r3] @ deref the reference (pointer) it returned, into a call-preserved register that will survive across the next call
movs r1, #1 @ arg for the v[1] operator[]
ldr r0, [r7, #4]
bl std::vector<int, std::allocator<int> >::operator[](unsigned int)
mov r3, r0
ldr r3, [r3] @ deref the returned reference
add r3, r3, r4 @ v[1] + v[0]
mov r0, r3 @ and copy into the return value reg because GCC didn't bother to add into it directly
adds r7, r7, #12 @ tear down the stack frame
mov sp, r7
pop {r4, r7, pc} @ and return by popping saved-LR into PC
@ and there's an actual implementation of the operator[] function
@ it's 15 instructions long.
@ But only one instance of this is needed for each type your program uses (vector<int>, vector<char*>, vector<my_foo>, etc.)
@ so it doesn't add up as much as each call-site
std::vector<int, std::allocator<int> >::operator[](unsigned int):
push {r7}
sub sp, sp, #12
...
如您所見,未優化的 GCC 更關心快速編譯時間,甚至比最簡單的事情更關心,例如避免無用的mov reg,reg
指令,甚至在評估一個表達式的代碼中也是如此。
如果您可以使用元數據制作整個 ELF 可執行文件,而不僅僅是您需要刻錄到 flash 的 .text +.rodata +.data,那么當然-g
調試信息對於文件大小非常重要,但基本上無關緊要,因為它沒有與運行時需要的部分混在一起,所以它只是放在磁盤上。
可以使用gcc -s
或strip
去除符號名稱和調試信息。
Stack-unwind 信息是代碼大小和元數據之間的一個有趣的權衡。 -fno-omit-frame-pointer
浪費額外的指令和寄存器作為幀指針,導致更大的機器代碼大小,但更小的.eh_frame
堆棧展開元數據。 (默認情況下, strip
不考慮“調試”信息,即使對於 C 程序也不是 C++,在非調試上下文中異常處理可能需要它。)
如何從 GCC/clang 程序集 output 中刪除“噪音”? 提到如何讓編譯器省略其中的一些: -fno-asynchronous-unwind-tables
省略 asm output 中的.cfi
指令,因此省略進入.eh_frame
部分的元數據。 另外-fno-exceptions -fno-rtti
和 C++ 可以減少元數據。 (反射的運行時類型信息占用空間。)
Linker 控制 alignment 節/ELF 段的選項也可能占用額外空間,與微小的可執行文件相關,但基本上是恆定的空間量,不隨程序大小縮放。 另請參閱最小可執行文件大小現在鏈接后比 2 年前大 10 倍,對於小程序?
快並不意味着小。 事實上,很大一部分速度優化都圍繞循環展開展開,這會大大增加代碼生成。
如果要優化大小,請使用-Os
,它等效於-O2
除了所有增加大小的優化(同樣,如循環展開)。
嘗試 -s -z noseparate-code(幾個月前在 stackoverflow 的某處找到,同時想知道為什么匯編中的簡單 hello world 是幾千字節而不是幾字節)
如果我沒記錯的話,-s 會刪除未使用的符號,-z noseparate-code 會從 elf-header 中刪除不需要的條目...(對 Gentoo 也很有用:)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.