本章聚焦於程式碼層級的效能改善技術。程式碼調整(Code Tuning)是指小規模的程式碼變更,而非大規模的設計重構。 這些技術看似與第 24 章的重構相似,但本質上是「反重構」——為了效能而犧牲內部結構的清晰度。 唯一可靠的法則是:在你的環境中實測每項調整的效果。

26.1 邏輯#

知道答案後就停止測試(Stop Testing When You Know the Answer)#

  • 短路求值(Short-Circuit Evaluation):C++ 和 Java 原生支援,當第一個條件已經能決定結果時,就不再評估後續條件。若語言不支援,可手動用巢狀 if 替代 and/or
  • 搜尋迴圈提前跳出:在陣列中找到目標值後立即 break,而非遍歷全部元素。實測顯示在 C++ 節省約 14%、Java 約 29%。

依頻率排列測試順序(Order Tests by Frequency)#

將最常見、最快通過的條件放在最前面,使一般情況能快速通過,罕見情況才進行較慢的判斷。

  • if-then-else 鏈重新排序的效果相當顯著(C# 節省 48%、Java 50%)。
  • 但對 case/switch 語句的效果則因語言而異(Java 反而無改善),原因在於不同編譯器對 case 的底層實作差異。

比較相似邏輯結構的效能(Compare Performance of Similar Logic Structures)#

同樣的測試可以用 caseif-then-else 實現。哪種更快完全取決於語言與編譯器:

  • Java 中 if-then-elsecase 快 6 倍。
  • Visual Basic 中 caseif-then-else 快 4 倍。

沒有任何「經驗法則」可以替代實際測量。

以查表取代複雜邏輯(Substitute Table Lookups)#

將複雜的布林條件鏈轉換為預先定義的查詢表(Lookup Table),不僅更容易維護,效能也更佳(C++ 節省 33%、VB 節省 50%)。

延遲求值(Lazy Evaluation)#

避免在啟動時計算所有可能用到的值。改為需要時才計算,計算後快取結果以供重複使用。適用於大型表格中只有小比例被實際存取的情況。

26.2 迴圈#

迴圈是程式的熱區(Hot Spots),微小的改善會因重複執行而放大效果。

迴圈外提(Unswitching)#

若迴圈內部的 if 條件在每次迭代都不變,就把條件移到迴圈外面,將一個迴圈拆成兩個各自獨立的迴圈。實測節省約 19-28%(Visual Basic 除外,幾乎無效果)。

外提後需要維護兩個平行的迴圈,增加了維護負擔。必須確保效能增益值得這項代價。

合併迴圈(Jamming / Fusion)#

將兩個遍歷相同元素的迴圈合併為一個,減少迴圈的額外開銷。C++ 節省 28%、PHP 32%。前提是兩個迴圈的索引範圍相同,且合併後不影響執行順序。

展開迴圈(Unrolling)#

在每次迭代中處理多個元素,減少迴圈控制的次數。

  • 展開一次:C++ 節省 34%、Java 43%,但 Python 反而慢了 27%。
  • 展開兩次:C++ 進一步提升至 42%、PHP 從 16% 提升至 31%。

主要風險是展開後的邊界處理容易出現差一錯誤(Off-by-One Error)。程式碼的可讀性也會大幅下降。

減少迴圈內的工作量(Minimizing Work Inside Loops)#

將迴圈內不變的運算提取到迴圈外部。例如,將複雜的指標解引用(Pointer Dereference)賦值給一個變數,既提升可讀性又提升效能(Java 節省 43%)。

哨兵值(Sentinel Values)#

在搜尋迴圈中,將目標值放在搜尋範圍的末端,這樣就能把雙重條件測試(是否找到 + 是否超出範圍)簡化為單一條件測試

  • 整數陣列搜尋:VB 節省 65%、Java 44%、C# 23%。
  • 浮點數陣列搜尋:效果稍減但仍顯著(VB 42%、Java 33%)。

使用哨兵值時必須在宣告陣列時預留額外空間,並謹慎選擇哨兵值。

將最忙碌的迴圈放在內層(Busiest Loop on the Inside)#

在巢狀迴圈中,讓迭代次數較多的迴圈放在內層,以減少總的迴圈初始化與控制次數。理論上,將外層 100 次、內層 5 次改為外層 5 次、內層 100 次,可從 600 次控制降至 505 次。實測改善從 4%(Python)到 34%(Java)不等。

強度折減(Strength Reduction)#

用較便宜的加法取代昂貴的乘法。當迴圈索引參與乘法運算時,可改為在每次迭代中累加固定增量。VB 節省 49%。

26.3 資料變換#

使用整數取代浮點數(Use Integers Rather Than Floating-Point Numbers)#

整數運算通常比浮點數快。將浮點迴圈索引改為整數,C++ 可節省 71%(效能比 3.5:1)、VB 高達 96%(25:1)。

使用最少的陣列維度#

將二維陣列改為一維陣列可以顯著提升效能。VB 節省 66%、Java 47%,但 C++ 和 C# 僅約 10%。

減少陣列引用(Minimize Array References)#

在內層迴圈中反覆存取的陣列元素,若其值在該迴圈不變,應先存入區域變數。VB 節省 20%。

使用輔助索引(Supplementary Indexes)#

  • 字串長度索引:像 Visual Basic 在字串開頭存放長度位元組,比 C 語言從頭掃描到結尾 \0 更快。此概念可推廣到任何可變長度資料型別。
  • 獨立平行索引結構:對大型資料排序與搜尋時,操作索引比操作資料本身更快。可將鍵值與指標存入輔助結構,在記憶體中完成搜尋,只在確定位置後才存取磁碟。

快取(Caching)#

將最常被存取的值儲存起來,避免重複計算。例如快取三角形斜邊計算結果,C++ 節省 74%(4:1)、Java 45%。快取的成功取決於產生新元素的成本快取命中率存取快取的成本之間的平衡。

26.4 運算式#

利用代數恆等式(Exploit Algebraic Identities)#

以等價但更便宜的運算取代昂貴的運算:

  • not a and not b 可改為 not (a or b),省下一次 not 運算。
  • sqrt(x) < sqrt(y) 可直接改為 x < y,省去兩次 sqrt() 呼叫——C++ 節省 99.9%。

強度折減(Use Strength Reduction)#

點擊展開:常見的強度折減替換列表
  • 乘法 → 加法
  • 指數運算 → 乘法
  • 三角函數 → 三角恆等式
  • long longlongint(注意原生長度的效能差異)
  • 浮點數 → 定點數或整數
  • 雙精度 → 單精度
  • 整數乘除以 2 → 位移運算

以多項式求值為例,將指數運算改為逐次累乘,VB 從 6.26 秒降至 0.16 秒(節省 97%)。但進一步用 Horner’s method 反而更慢——理論不一定等於實務。

在編譯期初始化(Initialize at Compile Time)#

若常數可以預先算出,就不要在執行期呼叫函式來計算。例如把 log(2) 替換為其常數值 0.69314718,節省約 28-39%。更進一步,可用整數測試完全取代浮點 log() 呼叫,C++ 節省 93%、Java 95%。

注意系統函式的成本(Be Wary of System Routines)#

系統數學函式通常為最高精度設計。若你只需要整數結果,不必使用為「把太空人送上月球」而設計的精度。例如,用一系列 if 測試取代 log() 函式的效能提升可達 15-20 倍。

使用正確的常數型別(Use the Correct Type of Constants)#

當常數與變數的型別不一致時,編譯器需要做型別轉換。好的編譯器會在編譯期完成,但較差的編譯器或直譯器會在執行期轉換,造成不必要的開銷。

預先計算結果(Precompute Results)#

預先計算有多種形式:

  • 在程式啟動時建立查詢表
  • 將計算結果存入檔案,執行時載入
  • 在迴圈外預算不變的子運算式
  • 首次計算後快取結果(結合快取策略)

以貸款還款計算為例,將複雜公式預算後存入查詢表,Java 節省 92%(10:1)。

消除共通子運算式(Eliminate Common Subexpressions)#

將重複出現的子運算式賦值給變數,既能提升效能,又能透過良好的變數命名增進可讀性。例如將 interestRate / 12.0 提取為 monthlyInterest 變數。

26.5 子程式#

良好的子程式分解是效能調整的強力工具。小而明確的子程式可以讓你集中優化,並且容易用低階語言改寫。

內聯展開子程式(Rewrite Routines Inline)#

在現代機器上,子程式呼叫的額外開銷已非常小。實測顯示,將字串複製子程式內聯展開,C++ 只節省 8%,而 Java 反而慢了 10%。C++ 可使用 inline 關鍵字讓編譯器處理,但不應期望顯著的效能提升。

26.6 用低階語言重寫程式碼#

當所有高階優化手段都已用盡,可考慮將熱區以低階語言改寫。典型流程:

  1. 以高階語言撰寫 100% 的應用程式
  2. 完整測試並驗證正確性
  3. 使用分析工具找出熱區——約 5% 的程式碼佔了 50% 的執行時間
  4. 將少數關鍵段落以低階語言改寫

以位元操作(Bit Manipulation)為例,Delphi 程式碼改寫為組合語言後節省 41%,C++ 則節省 29%。策略上可以先讓編譯器產生組合語言清單,再手動優化。

26.7 變得越多,事情反而越沒變#

第一版到第二版的十年間,電腦速度大幅提升——許多測試需要跑上一億次才能量測到差異。對許多桌面應用而言,本章討論的優化層級已經不再必要。

然而,嵌入式系統、即時系統和有嚴格速度/空間限制的系統,仍然需要這些技術。

每項調整的效果比十年前更難預測。影響因素包括程式語言、編譯器品牌與版本、程式庫版本、編譯器設定等。 堅持可量測的改善是抵抗過早優化誘惑的最佳方式——若不值得拿出分析工具來量測,就不值得犧牲可讀性。

更多資源#

  • Bentley, Jon.《Writing Efficient Programs》——程式碼調整的經典參考,涵蓋時間換空間與空間換時間的策略。
  • Bentley, Jon.《Programming Pearls》第二版,附錄 4 收錄了上述書籍的調整規則摘要。

要點#

  • 任何特定優化的效果都會因語言、編譯器和環境而大幅不同。不實測,就無法判斷對你的程式是加分還是扣分。
  • 第一個找到的優化往往不是最佳的——找到一個好方法後,繼續尋找更好的。
  • 程式碼調整如同核能——有人認為對可靠性和可維護性的損害太大而完全不碰,有人認為在適當防護下是有益的。若決定使用本章技術,務必謹慎為之