為什麼要檢視生成的中間碼#

程式碼在成為可執行檔之前,往往經過多次轉換:C/C++ 先經過 preprocessor 展開巨集,再編譯為組合語言,最後組譯成目的檔;Java 編譯為 JVM bytecode;而 lexyacc 等工具則將規則編譯成 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,可以看到迴圈內每次迭代都會:

  1. 建立新的 StringBuilder 物件
  2. s 附加進去
  3. 附加 " "
  4. 呼叫 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-Sjavap -c)或專門工具,取得中間碼的可讀表示
  • 預處理輸出對巨集展開除錯特別有效;組合語言 / bytecode 對效能問題特別有幫助