彷彿規則(the as-if rule)
當你要編譯器讓程式變快時,你給了它一個令人意外的許可:它可以丟掉你原始碼隱含的確切指令、換上完全不同的指令——只要你在觀看程式執行時分辨不出差別。那個許可就是彷彿規則(as-if rule),它是這個領域裡每一項最佳化底下的法律基礎。
C 與 C++ 標準描述一台抽象機器:一台假想、理想化的電腦,完全照你寫的、一步步執行你的程式。彷彿規則說,真正的編譯器不必忠實重現那些步驟;它只需產生一個「彷彿」在抽象機器上執行過的程式——也就是必須保留程式的可觀察行為。可觀察行為是一份精確、有限的清單:輸入與輸出的內容與順序(對 volatile 物件的讀寫、寫入檔案的資料、互動式 I/O),以及規範所定的程式終止。其餘一切都可動。編譯器可以刪除計算、重排操作、摺疊常數、把值只留在暫存器、甚至完全略過工作,只要抽象機器上的可觀察行為相同。
它重要在於:它一口氣解釋了為什麼最佳化器被允許做出它最驚人的把戲——那個從不執行的迴圈、那個沒有記憶體位置的變數、被重排的敘述。它們全都合法,正因為它們不改變觀察者能看見的東西。兩個關鍵的提醒讓這保持誠實。第一,這條規則只對「行為明確定義」的程式保留行為:如果你的程式有未定義行為,抽象機器就沒有可保留的明確行為,所以彷彿規則什麼都不約束、最佳化器可以做任何事(這正是 UB 驅動最佳化的核心)。第二,「可觀察」比你期望的更窄——沒人觀察的普通記憶體寫入不受保護,這正是為什麼 volatile 存在、用來把一次寫入標記為可觀察,也是為什麼時間與耗能根本不屬於可觀察行為。
int f(void) { int x = 0; for (int i = 0; i < 1000000; i++) x += 1; // 可觀察?沒人觀看 x 或 i return x; } // 編譯器可以彷彿等價地輸出: int f(void) { return 1000000; } // 沒有迴圈執行;可觀察行為相同
迴圈被刪除,因為沒有可觀察行為取決於它的執行——只有回傳值重要。
彷彿規則只保護「行為明確」的程式:未定義行為意味著抽象機器沒有可保留的行為,所以這條規則什麼都不約束——而「可觀察」不含時間、耗能與單純未被觀察的記憶體寫入(這正是 volatile 存在的原因)。