為什麼要檢視生成的中間碼#
程式碼在成為可執行檔之前,往往經過多次轉換:C/C++ 先經過 preprocessor 展開巨集,再編譯為組合語言,最後組譯成目的檔;Java 編譯為 JVM bytecode;而 lex、yacc 等工具則將規則編譯成 C/C++ 程式碼。檢視這些中間產物,能幫助你理解編譯器「真正做了什麼」,進而定位錯誤。
檢視預處理後的 C/C++ 程式碼#
- 使用
-E(Unix)或/E(MSVC)選項輸出預處理結果 - 在 Unix 上也可直接呼叫
cpp預處理器 - 建議將輸出重導至檔案,方便在編輯器中搜尋
實際範例:巨集展開除錯#
#define PI 3.1415926535897932384626433832795;
double toDegrees = 360 / 2 / PI;
double toRadians = 2 * PI / 360;編譯時出現難以理解的語法錯誤。查看預處理輸出後,可以清楚看到巨集定義結尾多了一個 分號,展開後導致語法不合法。
這個技巧對於除錯複雜巨集和第三方標頭檔中的展開錯誤特別有效。當展開結果很大時,可搜尋失敗行附近的非巨集識別符(如變數名稱)來定位問題位置。
移除 #line 指令#
- 預處理輸出中的
#line指令會讓編譯器報告原始檔案的行號 - 若想在預處理輸出中直接定位錯誤,使用
-P(Unix)或/EP(MSVC)抑制#line輸出
檢視組合語言與機器碼#
當你被一個低階錯誤困住時,查看生成的機器指令往往能帶來頓悟:
- 產生組合語言:Unix 使用
-S,MSVC 使用/Fa - GCC Intel 語法:加上
-masm=intel可產生較易讀的 Intel 格式 - JVM bytecode:對 class 檔案執行
javap -c ClassName
實際範例:Java 字串串接效能問題#
class LongString {
public static void main(String[] args) {
String s = "";
for (int i = 0; i < 100000; i++)
s += " ";
}
}這段程式執行超過 9 秒。透過 javap -c 檢視 bytecode,可以看到迴圈內每次迭代都會:
- 建立新的 StringBuilder 物件
- 將
s附加進去 - 附加
" " - 呼叫
toString()轉換回 String
等同於每次都執行:
s = new StringBuilder().append(s).append(" ").toString();改為在迴圈外建立 StringBuilder,程式幾乎瞬間完成:
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 100000; i++)
sb.append(" ");
s = sb.toString();雖然組合語言看起來很晦澀,但把指令對應回原始碼,通常就能猜出大致的行為,這往往就夠用了。
重點回顧#
- 檢視自動產生的中間碼,能幫助你理解編譯與執行時期問題的根本原因
- 善用 編譯器選項(
-E、-S、javap -c)或專門工具,取得中間碼的可讀表示 - 預處理輸出對巨集展開除錯特別有效;組合語言 / bytecode 對效能問題特別有幫助