化等級(jí)詳解:從-O0到-Os的適用場(chǎng)景與取舍)
1. 先從基礎(chǔ)說(shuō)起-O 優(yōu)化到底在優(yōu)化什么編譯器gcc、clang、msvc 這些本質(zhì)上是一個(gè)翻譯官把人類能讀懂的 C/C 代碼翻譯成機(jī)器能執(zhí)行的匯編指令。但翻譯和翻譯之間差距很大——?jiǎng)側(cè)腴T的翻譯可能逐字逐句硬譯老練的翻譯會(huì)調(diào)整語(yǔ)序、精簡(jiǎn)表達(dá)、合并重復(fù)內(nèi)容讓譯文更流暢。編譯器的 -O 優(yōu)化就是干這個(gè)的只是它優(yōu)化的不是語(yǔ)言的優(yōu)美程度而是執(zhí)行速度、代碼體積、功耗這些硬指標(biāo)。很多剛接觸嵌入式或者 Linux 下 C 開發(fā)的人第一次看到gcc -O2 -o app main.c這種命令都會(huì)愣一下這個(gè)-O2是什么為什么有時(shí)候是-O0有時(shí)候是-Os還有-Og、-O1、-O3它們之間到底差在哪先說(shuō)一句最關(guān)鍵的話-O 后面對(duì)應(yīng)的數(shù)字越小編譯速度越快、生成的代碼越容易調(diào)試數(shù)字越大編譯時(shí)間越長(zhǎng)、運(yùn)行速度越快但出問題的概率也越高。這里面每個(gè)級(jí)別的取舍都值得掰開揉碎講清楚因?yàn)檫x錯(cuò)優(yōu)化等級(jí)輕則程序崩潰重則線上事故、數(shù)據(jù)錯(cuò)亂。2. 一份速查表主流 -O 選項(xiàng)到底有哪些我先把目前 gcc 和 clang 里最常見的優(yōu)化等級(jí)列出來(lái)方便你對(duì)照著看后面每一節(jié)的內(nèi)容。順手加一句msvc微軟的 C 編譯器也有類似的/O1、/O2、/Ox選項(xiàng)原理和 gcc 基本互通看懂了 gcc 的換到 Visual Studio 里也能舉一反三。選項(xiàng)全稱含義核心目標(biāo)常用場(chǎng)景-O0不優(yōu)化調(diào)試體驗(yàn)優(yōu)先Debug 版本、斷點(diǎn)調(diào)試、上課學(xué)編譯原理-O1輕度優(yōu)化在編譯速度和運(yùn)行速度間找平衡快速驗(yàn)證邏輯、舊編譯器兼容、部分嵌入式工程-O2推薦優(yōu)化穩(wěn)定且全面的性能提升絕大多數(shù)項(xiàng)目的 Release 默認(rèn)選項(xiàng)-O3激進(jìn)優(yōu)化極致性能不限編譯時(shí)間數(shù)值計(jì)算、音視頻編解碼、科學(xué)仿真-Os優(yōu)化體積生成盡量小的代碼單片機(jī) Flash 受限、固件、內(nèi)核模塊-Og優(yōu)化調(diào)試優(yōu)化但保留完整調(diào)試信息開發(fā)中后期、調(diào)試 Release 問題-Ofast無(wú)視標(biāo)準(zhǔn)-O3基礎(chǔ)上放寬標(biāo)準(zhǔn)約束追求速度且不 care 精度邊界約束的場(chǎng)景慎用每家編譯器對(duì)各個(gè)等級(jí)的精細(xì)實(shí)現(xiàn)略有差異但整體邏輯高度一致。下面我把-O0到-Os逐個(gè)講透每個(gè)等級(jí)背后的“為什么”才是真正值錢的部分。2.1 中間表示IR這個(gè)概念你得先知道要理解優(yōu)化級(jí)別最好先知道編譯器內(nèi)部干活時(shí)的核心結(jié)構(gòu)。gcc 在把 C 代碼變成匯編之前會(huì)先翻譯成一種叫 GIMPLE 的中間表示clang 則叫 LLVM IR。優(yōu)化就是在這個(gè)中間表示上做變換比如“這行計(jì)算和后面那行重復(fù)了刪掉一個(gè)”“這個(gè)變量只有一處用直接替換成值”。不同的優(yōu)化等級(jí)就是打開不同數(shù)量和種類的變換開關(guān)。這個(gè)和做菜挺像-O0相當(dāng)于把菜洗好切好就端上桌能看但沒加工-O1相當(dāng)于大火快炒熟了但沒入味-O2相當(dāng)于精心烹飪色香味俱全-O3相當(dāng)于用名貴食材和高湯反復(fù)熬制極致好吃但費(fèi)時(shí)費(fèi)力還可能把食材本味搞沒。3. -O0不優(yōu)化才是最適合調(diào)試的-O0是 gcc 的默認(rèn)優(yōu)化等級(jí)。如果你直接執(zhí)行g(shù)cc main.c -o app等效于gcc -O0 main.c -o app。它的核心原則是保持可觀察行為與源代碼嚴(yán)格一致。這是什么意思舉個(gè)例子int add(int a, int b) { int tmp a b; return tmp; }-O0編譯時(shí)gcc 會(huì)老老實(shí)實(shí)給tmp分配一個(gè)??臻g先算ab存進(jìn)去再讀出來(lái)返回。每一步都對(duì)應(yīng)源碼里的每一行中間變量一個(gè)不少。你在調(diào)試器里打斷點(diǎn)想查看tmp的值它就在那里清清楚楚。但如果用-O2編譯同一段代碼tmp這個(gè)變量可能直接消失了——編譯器發(fā)現(xiàn)tmp只是轉(zhuǎn)了一道手完全可以把a(bǔ)b的值直接放進(jìn)返回值寄存器中間那個(gè)臨時(shí)變量既沒存活期也沒副作用。這時(shí)候你在調(diào)試器里想查看tmpgdb 會(huì)告訴你value optimized out。這就是為什么調(diào)試階段尤其是剛寫完一個(gè)模塊需要逐步確認(rèn)邏輯時(shí)一定要用-O0。3.1 -O0 也不是字面意義上的“不優(yōu)化”很多初學(xué)者誤以為-O0是編譯器什么都不干直接把代碼機(jī)械翻譯。實(shí)際上gcc 在-O0下仍會(huì)做一些必要處理比如把常量表達(dá)式折疊const int x 10; int y x * 2;這種在編譯期就能算出y 20的還是會(huì)算出來(lái)。做一些最基礎(chǔ)的指令選擇比如用乘法指令還是移位加加法。對(duì)明顯的棧幀布局做規(guī)劃。只是這些操作不會(huì)改變調(diào)試體驗(yàn)也不會(huì)改變變量生命周期所以感知不到??梢园?O0理解為“編譯器只做能不做就不做的保守翻譯”而不是“完全不翻譯”。3.2 什么時(shí)候該用 -O0除了日常調(diào)試還有兩類場(chǎng)景我強(qiáng)烈建議用-O0第一類是出問題需要精確復(fù)現(xiàn)現(xiàn)場(chǎng)時(shí)。比如線上程序崩潰你拉下來(lái) Core Dump 文件這時(shí)候如果線上是-O2編譯的崩潰棧里好多函數(shù)參數(shù)都顯示optimized out排查效率極低。很多團(tuán)隊(duì)會(huì)在現(xiàn)場(chǎng)保留一份-O0的調(diào)試版本就是為了應(yīng)對(duì)這種問題。第二類是嵌入式裸機(jī)開發(fā)、外設(shè)寄存器操作多、時(shí)序要求嚴(yán)格的場(chǎng)景。寄存器寫操作和硬件行為強(qiáng)綁定編譯器但凡給你“聰明”一把比如把一個(gè) volatile 讀操作優(yōu)化掉整個(gè)硬件驅(qū)動(dòng)就廢了。雖然標(biāo)準(zhǔn)做法是給寄存器指針加上 volatile但在-O0下做初期功能驗(yàn)證至少能少一層“編譯器替你亂搞”的風(fēng)險(xiǎn)。4. -O1保守但不平庸的平衡點(diǎn)-O1常常被新手忽略因?yàn)榇蠹叶级⒅?O2和-O3。但-O1在特定場(chǎng)景下非常有用尤其是編譯速度敏感型項(xiàng)目。-O1開啟的優(yōu)化主要包括死代碼消除DCE把永遠(yuǎn)不會(huì)執(zhí)行到的代碼分支刪掉。死存儲(chǔ)消除變量賦值后沒被讀編譯器直接不生成這個(gè)寫操作。局部公共子表達(dá)式消除CSE同一表達(dá)式在一個(gè)基本塊內(nèi)重復(fù)計(jì)算多次時(shí)只算一次。分支預(yù)測(cè)優(yōu)化根據(jù)靜態(tài)信息調(diào)整 if/else 排列讓大概率走的分支更緊湊。部分寄存器分配基礎(chǔ)優(yōu)化減少不必要的內(nèi)存讀寫。這些優(yōu)化有個(gè)共同特征它們不會(huì)改變程序的可觀察行為而且在絕大多數(shù)架構(gòu)上都是純賺不虧。不需要做復(fù)雜的跨函數(shù)分析也不會(huì)引入激進(jìn)變換所以編譯速度損失不大運(yùn)行時(shí)收益卻很明顯。我有一個(gè)實(shí)際經(jīng)驗(yàn)一個(gè)編譯需要 10 分鐘的大型 C 項(xiàng)目用-O1比用-O2編譯時(shí)間能縮短 25% 左右運(yùn)行性能也就差 10%~15%。在持續(xù)集成CI流程里如果每次提交都要跑大量單元測(cè)試用-O1編譯測(cè)試版本能顯著減少等待時(shí)間而且測(cè)試結(jié)果比-O0更接近線上行為。4.1 -O1 適合誰(shuí)做腳本語(yǔ)言解釋器、即時(shí)編譯器這類重編譯速度不重運(yùn)行速度的項(xiàng)目。大項(xiàng)目 CI 流水線的中間產(chǎn)物。需要在老機(jī)器上、內(nèi)存吃緊的構(gòu)建環(huán)境中編譯的場(chǎng)景。5. -O2默認(rèn)的王者生產(chǎn)環(huán)境的標(biāo)配如果你問我剛?cè)肼氁患夜灸玫揭粋€(gè)未知項(xiàng)目該怎么編譯我會(huì)毫不猶豫告訴你先試-O2。絕大多數(shù)企業(yè)級(jí) C/C 項(xiàng)目的 Release 版本都用-O2這是行業(yè)默認(rèn)值。-O2在-O1基礎(chǔ)上增加的核心優(yōu)化包括函數(shù)內(nèi)聯(lián)inlining被頻繁調(diào)用的小函數(shù)直接把函數(shù)體“展開”到調(diào)用處省去 call/ret 的開銷。循環(huán)展開把循環(huán)體復(fù)制多份減少循環(huán)控制語(yǔ)句執(zhí)行次數(shù)。全局公共子表達(dá)式消除不止在局部跨塊、跨循環(huán)都要消除重復(fù)計(jì)算。更激進(jìn)的寄存器分配盡可能把變量放進(jìn)寄存器而不是棧里。指令調(diào)度與重排讓 CPU 流水線更順暢地執(zhí)行指令。尾部調(diào)用優(yōu)化把遞歸調(diào)用變成循環(huán)棧復(fù)用。這些優(yōu)化的綜合效果非??捎^。我做過(guò)一次實(shí)測(cè)后面會(huì)放數(shù)據(jù)一段包含矩陣乘法和字符串處理的代碼-O2比-O0快 3~5 倍而且編譯時(shí)間增加只有 2 倍左右性價(jià)比極高。5.1 為什么生產(chǎn)環(huán)境偏偏選中 -O2這里面有一個(gè)關(guān)鍵因素實(shí)踐經(jīng)驗(yàn)的積累。-O3帶來(lái)的很多激進(jìn)優(yōu)化在實(shí)際項(xiàng)目中偶爾會(huì)引入“編譯器優(yōu)化導(dǎo)致的 bug”比如浮點(diǎn)重排、順序假設(shè)變化。而-O2經(jīng)過(guò)十幾年的工程檢驗(yàn)bug 已經(jīng)被磨得很少行為相對(duì)可預(yù)期。另一個(gè)因素是內(nèi)核和系統(tǒng)庫(kù)的默認(rèn)選項(xiàng)。Linux 內(nèi)核編譯默認(rèn)使用-O2新版內(nèi)核實(shí)際更復(fù)雜但大體在-O2附近glibc 官方推薦也是-O2。生態(tài)主流在哪大家跟著用遇到問題的概率最低。5.2 但 -O2 也有“坑”最典型的是一個(gè) C 語(yǔ)言里的“時(shí)序陷阱”如果代碼里依賴了 int 溢出的行為或者依賴了有符號(hào)變量左移的行為-O2下的優(yōu)化可能會(huì)產(chǎn)生不一樣的結(jié)果。比如int func(int x) { int y x * 4; return y 2; }-O0下執(zhí)行可能是先乘法再移位-O2下編譯器直接優(yōu)化成return x;因?yàn)槌艘?4 再除以 4 恒等于自身不考慮溢出的情況下。這個(gè)優(yōu)化本身沒問題但如果你原本希望通過(guò)移位完成某種比特操作、或者對(duì)溢出后的符號(hào)位做了假設(shè)那么優(yōu)化后的代碼就跟期望不一致了。解決辦法只有一個(gè)別依賴 undefined behavior未定義行為和 implementation-defined behavior實(shí)現(xiàn)定義行為。這是 C/C 里最核心的規(guī)矩后面我會(huì)專門講。6. -O3性能怪獸但請(qǐng)系好安全帶-O3是在-O2基礎(chǔ)上繼續(xù)增加優(yōu)化gcc 和 clang 的主要增量包括更多函數(shù)內(nèi)聯(lián)包括更大、更深層的函數(shù)也強(qiáng)塞進(jìn)調(diào)用處。自動(dòng)向量化auto-vectorization把循環(huán)里的標(biāo)量運(yùn)算轉(zhuǎn)化成 SIMD 指令比如 x86 的 SSE/AVX一次算 4 個(gè)或 8 個(gè) float。更加激進(jìn)的循環(huán)變換循環(huán)交換、循環(huán)展開、循環(huán)合并等。預(yù)測(cè)性函數(shù)內(nèi)聯(lián)編譯器根據(jù)熱路徑估計(jì)自動(dòng)擴(kuò)大內(nèi)聯(lián)范圍??邕^(guò)程優(yōu)化IPA跨文件、跨函數(shù)做全局?jǐn)?shù)據(jù)流分析。-O3在數(shù)值密集型計(jì)算里的收益尤其顯著。比如矩陣乘法、信號(hào)處理、圖像濾鏡用上自動(dòng)向量化之后能再快 20%~50%。而且現(xiàn)代編譯器的向量化能力一年比一年強(qiáng)這份收益還在上升。但是-O3也有明顯的代價(jià)和風(fēng)險(xiǎn)編譯時(shí)間暴漲。我自己編譯過(guò)一個(gè)包含大量模板的 C 項(xiàng)目-O3比-O2編譯時(shí)間增加了 80%。代碼體積變大。內(nèi)聯(lián)和循環(huán)展開意味著更多機(jī)器指令緩存壓力可能抵消性能收益。更激進(jìn)的浮點(diǎn)變換。比如把a(bǔ)*b a*c優(yōu)化成a*(bc)雖然代數(shù)上相等但浮點(diǎn)運(yùn)算的舍入誤差不同結(jié)果最后幾位可能有差異。自動(dòng)向量化引入的潛在越界問題。有些循環(huán)在邊界處理上原本依賴“多算一次”但不會(huì)出錯(cuò)向量化后可能在邊界處多讀了幾個(gè)字節(jié)引發(fā)崩潰。6.1 什么時(shí)候放心用 -O3純數(shù)值計(jì)算程序沒有強(qiáng) I/O、沒有系統(tǒng)調(diào)用密集邏輯、沒有依賴未定義行為的代碼。對(duì)性能有硬指標(biāo)要求的模塊比如視頻編解碼器、物理引擎。不介意調(diào)試?yán)щy且測(cè)試用例覆蓋充分。6.2 什么時(shí)候別用 -O3嵌入式裸機(jī)程序Flash 放不下。網(wǎng)絡(luò)協(xié)議棧、通信模塊對(duì)時(shí)序敏感且對(duì)代碼行為確定性要求極高。大量使用遞歸或鏈表指針跳轉(zhuǎn)的程序。這類程序難以向量化-O3收益不大編譯時(shí)間倒是實(shí)實(shí)在在增加了。7. -Os把“瘦身”進(jìn)行到底單片機(jī)開發(fā)者對(duì)-Os應(yīng)該不陌生。-Os的核心目標(biāo)是生成最小體積的機(jī)器碼它會(huì)基于-O2的優(yōu)化集合作調(diào)整凡是會(huì)導(dǎo)致代碼變大的優(yōu)化都會(huì)被壓制或削弱。典型區(qū)別函數(shù)內(nèi)聯(lián)只在被調(diào)函數(shù)極小且調(diào)用次數(shù)不多時(shí)才會(huì)執(zhí)行。循環(huán)展開通常被禁用因?yàn)檎归_通常意味著代碼變多。公共子表達(dá)式消除照做因?yàn)樗ǔD軠p少計(jì)算但未必增大體積。優(yōu)先選擇體積更小的指令序列哪怕稍慢一點(diǎn)。實(shí)際效果有多大我做過(guò)一次 Cortex-M 內(nèi)核的裸機(jī)程序?qū)Ρ韧环荽a-O2生成 28 KB-Os生成 21 KB體積減小了 25%運(yùn)行時(shí)間只多出 3% 左右。這對(duì)于 Flash 只有 64 KB 的 MCU 來(lái)說(shuō)絕對(duì)是救命級(jí)的選擇。7.1 -Os 的隱蔽缺陷代碼小了但有個(gè)問題容易被忽略部分“空間換時(shí)間”的優(yōu)化被關(guān)閉后程序?qū)χ袛囗憫?yīng)時(shí)間的波動(dòng)會(huì)更敏感。在一些有硬實(shí)時(shí)的場(chǎng)景比如電機(jī)控制、電力電子你要搞清楚項(xiàng)目到底對(duì)“確定性”的要求有多高。另外調(diào)試器配合-Os會(huì)很難用——變量被復(fù)用和重排的情況比-O2更嚴(yán)重因?yàn)榫幾g器為了省空間會(huì)把一個(gè)棧槽反復(fù)用于多個(gè)變量。所以用-Os做 Release用-O0做 Debug這條原則千萬(wàn)不能變。8. -Og一個(gè)折中的“調(diào)試友好優(yōu)化”很多開發(fā)者在-O2下遇到 bug硬著頭皮用 gdb 看匯編一點(diǎn)一點(diǎn)摳效率極低。-Og就是為這個(gè)場(chǎng)景設(shè)計(jì)的在優(yōu)化和調(diào)試體驗(yàn)之間取一個(gè)中間值。-Og做了-O1級(jí)別的優(yōu)化但只選擇那些不太影響調(diào)試體驗(yàn)的優(yōu)化項(xiàng)。比如它仍然會(huì)做死代碼消除但不會(huì)做破壞調(diào)試信息的寄存器重映射和重排。在-Og下編譯的程序斷點(diǎn)、單步、變量查看的體驗(yàn)接近-O0而性能又比-O0好不少。我現(xiàn)在的個(gè)人習(xí)慣是開發(fā)中期用-Og替代-O0。前期功能還沒寫完邏輯頻繁改動(dòng)用-O0進(jìn)入聯(lián)調(diào)和 bug 修復(fù)期改-Og既接近最終 Release 的行為又能保持不錯(cuò)的調(diào)試體驗(yàn)兩邊兼顧。9. 實(shí)測(cè)數(shù)據(jù)同一段代碼不同等級(jí)的差距說(shuō)再多理論不如看數(shù)據(jù)。我寫了一段綜合的小型 benchmark包含矩陣乘法、快速排序、字符串哈希和遞歸斐波那契在 x86-64 Ubuntu gcc 12 下編譯運(yùn)行記錄編譯時(shí)間和運(yùn)行時(shí)間取 10 次平均值代碼體積用size命令查看。優(yōu)化等級(jí)編譯時(shí)間運(yùn)行時(shí)間可執(zhí)行文件體積-O00.42s4.87s34 KB-O10.55s2.10s28 KB-O20.78s1.13s30 KB-O31.62s0.86s45 KB-Os0.72s1.52s22 KB-Og0.61s2.41s27 KB幾個(gè)關(guān)鍵結(jié)論-O2相比-O0快了 4.3 倍這個(gè)數(shù)字完全不夸張。-O3比-O2快約 24%但編譯時(shí)間翻了一倍多。遞歸斐波那契這種簡(jiǎn)單遞歸函數(shù)-O3幾乎沒優(yōu)勢(shì)自動(dòng)向量化幫不上忙但在矩陣乘法部分-O3的優(yōu)勢(shì)就體現(xiàn)出來(lái)了。-Os體積最小運(yùn)行時(shí)間比-O2慢約 35%但體積少了 27%。所以說(shuō)“我該用哪個(gè)優(yōu)化等級(jí)”沒有標(biāo)準(zhǔn)答案取決于瓶頸是 CPU、是二進(jìn)制體積、還是編譯時(shí)間。這也是為什么大型項(xiàng)目通常會(huì)讓構(gòu)建系統(tǒng)允許你隨時(shí)切換優(yōu)化等級(jí)參數(shù)的。10. 優(yōu)化和未定義行為90% 的“編譯器優(yōu)化 bug”真相說(shuō)一個(gè)我這些年來(lái)一直被問的問題“老師我的程序在 -O2 下崩了在 -O0 下好好的是不是編譯器有 bug”我的回答永遠(yuǎn)是極少數(shù)情況是編譯器 bug95% 的可能是你的代碼觸碰了未定義行為UB, undefined behavior。C 和 C 標(biāo)準(zhǔn)里有一堆“雷區(qū)”操作標(biāo)準(zhǔn)沒有定義結(jié)果包括但不限于有符號(hào)整數(shù)溢出比如int x INT_MAX; x 1;。解引用空指針。數(shù)組越界訪問。讀取未初始化的變量。符合類型之間用memcpy之外的邏輯做強(qiáng)制轉(zhuǎn)換union 在某些場(chǎng)景下也算 UB。有符號(hào)整數(shù)左移溢出或移位次數(shù)超過(guò)類型位寬。在同一表達(dá)式里對(duì)一個(gè)變量先讀后寫且沒有序列點(diǎn)分隔如i i 1;。為什么-O0下這些代碼“看起來(lái)正常”因?yàn)?O0基本是機(jī)械翻譯你的匯編指令恰好做了你預(yù)期的事。但-O2下編譯器會(huì)根據(jù)“無(wú) UB 的假設(shè)”做推導(dǎo)優(yōu)化。比如int func(int x) { if (x 1 x) { return 1; } return 0; }有符號(hào)整數(shù)x1x這個(gè)條件在標(biāo)準(zhǔn)語(yǔ)義下沒有任何一個(gè)x會(huì)成立如果x1溢出本身就是 UB編譯器假定不會(huì)發(fā)生所以編譯器直接把整個(gè) if 分支刪了函數(shù)直接返回 0。你在-O0下還能看到那個(gè)分支存在但在-O2下它已經(jīng)蒸發(fā)了。這個(gè)例子在真實(shí)工程里經(jīng)常演化成一個(gè)防御性的邊界檢查被編譯器優(yōu)化沒了線上數(shù)據(jù)異常時(shí)沒攔住。怎么破三個(gè)方向代碼層面消除 UB有符號(hào)溢出改用無(wú)符號(hào)運(yùn)算或__builtin_add_overflow這類內(nèi)建函數(shù)。使用 UBSanUndefinedBehaviorSanitizer在-O1或-O2下編譯時(shí)加-fsanitizeundefined程序會(huì)在觸發(fā) UB 時(shí)打印診斷信息。別跟編譯器較勁不要寫“靠具體機(jī)器的匯編行為活下來(lái)”的代碼跨平臺(tái)和跨優(yōu)化等級(jí)都會(huì)炸。順帶推薦 CS 愛好者必試的組合gcc -O2 -fsanitizeaddress,undefined -g -o app main.cAddressSanitizer 檢查內(nèi)存問題UndefinedBehaviorSanitizer 把關(guān)未定義行為。這兩兄弟在 CI 里加一道能擋掉 80% 的“神奇崩潰”。11. 工具鏈與實(shí)操怎么查編譯器實(shí)際做了哪些優(yōu)化選了某個(gè)優(yōu)化等級(jí)后你其實(shí)可以親眼看看編譯器到底對(duì)你的代碼做了什么。最直觀的方法是用-S參數(shù)生成匯編文件gcc -O2 -S main.c -o main.s然后打開main.s能看到優(yōu)化后的匯編。但人讀匯編效率太低我用得最多的是查 GCC 的優(yōu)化 dump 文件gcc -O2 -fdump-tree-all -c main.c -o main.o這個(gè)命令會(huì)生成一大堆.c.xxx文件記錄每一個(gè)優(yōu)化 pass 前后的中間表示。剛開始看會(huì)頭暈但配合diff對(duì)比不同 pass 的差異能看到變量什么時(shí)候被消除、表達(dá)式什么時(shí)候被折疊、循環(huán)什么時(shí)候被變換。這是理解“優(yōu)化在做什么”最好的教材。另一個(gè)實(shí)用的工具是-fopt-infogcc -O3 -fopt-info-vec -c matmul.c -o matmul.o如果編譯器對(duì)某個(gè)循環(huán)做了向量化終端會(huì)打印向量化的詳細(xì)信息。用它可以驗(yàn)證你的代碼是否真的跑上了 SIMD是排查性能瓶頸的一把好手。12. 常見問題速查與我的個(gè)人建議最后整理一個(gè)速查表把日常被問得最多的問題統(tǒng)一回答問題答案程序在 -O2 崩潰在 -O0 正常是編譯器 bug 嗎先懷疑自己的 UB用 UBSan 檢查再懷疑編譯器為什么 gdb 里變量顯示 optimized out因?yàn)樵撟兞恳驯粌?yōu)化消失改用 -O0 或 -Og發(fā)布版本該用 -O2 還是 -O3默認(rèn) -O2數(shù)值密集且有完整測(cè)試再用 -O3Flash 不夠了怎么壓縮代碼用 -Os再配合 -ffunction-sections -fdata-sections 鏈接器 --gc-sections想快又不想可執(zhí)行文件太大-O2 是平衡點(diǎn)-Os 適合體積敏感的我的嵌入式中斷函數(shù)在 -O2 下失效了檢查是否漏了 volatile或用了不規(guī)范的寄存器訪問同一套代碼換了編譯器版本性能下降對(duì)比各版本優(yōu)化差異用 -fopt-info 查優(yōu)化決策再說(shuō)兩個(gè)很多項(xiàng)目里都容易踩的具體坑??右辉陬^文件里定義非 inline 函數(shù)。-O2下編譯器自動(dòng)內(nèi)聯(lián)能掩蓋一部分問題但當(dāng)你切到-O0或者另一個(gè)編譯單元時(shí)多重定義鏈接錯(cuò)誤立刻爆出來(lái)。解決方法是加static inline或者放到 .c 文件里定義、頭文件只放聲明。坑二把volatile關(guān)鍵字當(dāng)成“什么都別優(yōu)化”的萬(wàn)能藥。很多人看到某段寄存器代碼被優(yōu)化掉了第一反應(yīng)是加volatile。這確實(shí)能讓編譯器不再緩存本次讀取但volatile不保證原子性也不保證內(nèi)存屏障語(yǔ)義多線程下該崩還是崩。多線程同步要用的不是 volatile而是std::atomic/atomic_flag之類的原子原語(yǔ)。如果你是個(gè)剛接觸編譯優(yōu)化的開發(fā)者我建議按照這個(gè)順序來(lái)練習(xí)先用-O0寫功能保證邏輯對(duì)。切-O2跑測(cè)試發(fā)現(xiàn)差異。遇到優(yōu)化導(dǎo)致的問題用 UBSan/ASan 定位回頭改代碼。跑一遍-O3和-Os對(duì)比性能和體積理解取舍。再用-fopt-info和-S看匯編搞清楚編譯器每一分性能提升從哪來(lái)。這套流程走完你對(duì) -O 優(yōu)化的理解會(huì)遠(yuǎn)超絕大多數(shù)“能編譯能運(yùn)行就行”的同行。我在實(shí)際項(xiàng)目中體會(huì)最深的一點(diǎn)是編譯器優(yōu)化的本質(zhì)是“基于安全假設(shè)的大膽推導(dǎo)”。它敢刪代碼、敢重排語(yǔ)句、敢合并分支是因?yàn)樗J(rèn)你的代碼完全符合語(yǔ)言標(biāo)準(zhǔn)。只要你守住標(biāo)準(zhǔn)這層底線-O2 就是你最省心的朋友一旦越界它就是你最難纏的對(duì)手。從-O0到-Og再到-O2每一步都是你在“調(diào)試體驗(yàn)、運(yùn)行性能、代碼體積”三者之間的主動(dòng)取舍——理解得越深這個(gè)取舍就越有價(jià)值。