效能調整(Performance Tuning)在軟體開發史上一直是爭議性的議題。從 1960 年代資源極度受限的年代,到後來硬體進步帶來的觀念轉變,再到嵌入式系統與直譯式語言重新喚起效能意識——每個世代的開發者都在「效能」與「可讀性」之間拉鋸。本章從策略層面探討效能議題:什麼是效能、它有多重要、以及達成效能目標的整體方法。
25.1 效能概述#
程式碼調整只是改善效能的方式之一,而且往往不是最有效的。在投入程式碼層級的最佳化之前,應先考量更高層次的選項。
品質特性與效能#
使用者關心的是實際體驗,而非程式碼的精巧程度。他們通常更在意吞吐量(Throughput)、介面易用性、準時交付與系統穩定性,而不是原始的執行速度。
效能與程式碼速度只有鬆散的關聯。為了速度而犧牲其他品質特性,可能反而傷害整體效能。
效能改善的七個層次#
在選擇程式碼調整之前,應從以下每個角度思考效能:
| # | 層次 | 說明 |
|---|---|---|
| 1 | 程式需求 | 效能需求經常被過度描述。放寬不必要的效能要求可能省下巨額成本 |
| 2 | 程式設計 | 高層架構決定效能上限。設定各子系統的資源目標,並保持高度模組化以便日後替換 |
| 3 | 類別與常式設計 | 選擇適當的資料型別與演算法(如快速排序 vs. 氣泡排序) |
| 4 | 作業系統互動 | 系統呼叫代價高昂,涉及上下文切換。應留意隱藏的系統呼叫 |
| 5 | 程式碼編譯 | 好的最佳化編譯器可帶來 40% 以上的全面提升,有時比手動調整更有效 |
| 6 | 硬體 | 對內部系統而言,升級硬體可能是最便宜的方案 |
| 7 | 程式碼調整 | 修改正確的程式碼使其更有效率,屬於小規模的戰術性變更 |
Jon Bentley 指出,若各層次的改善彼此獨立,理論上效能提升可達到百萬倍的乘積效果(每層 10 倍 x 6 層)。
flowchart TB
A["1. 程式需求"] --> D1{"效能足夠?"}
D1 -->|否| B["2. 程式設計"]
D1 -->|是| DONE["完成"]
B --> D2{"效能足夠?"}
D2 -->|否| C["3. 類別與常式設計"]
D2 -->|是| DONE
C --> D3{"效能足夠?"}
D3 -->|否| D["4. 作業系統互動"]
D3 -->|是| DONE
D --> D4{"效能足夠?"}
D4 -->|否| E["5. 程式碼編譯"]
D4 -->|是| DONE
E --> D5{"效能足夠?"}
D5 -->|否| F["6. 硬體"]
D5 -->|是| DONE
F --> D6{"效能足夠?"}
D6 -->|否| G["7. 程式碼調整"]
D6 -->|是| DONE
G --> DONE25.2 程式碼調整簡介#
程式碼調整的魅力在於——它看似「違反自然法則」,將 20 微秒的常式壓縮到 2 微秒,令人極為滿足。然而,高效的程式碼不等於「好」的程式碼。
柏拉圖法則(The Pareto Principle)#
80/20 法則在效能最佳化中尤其適用:
- Barry Boehm 指出,20% 的常式消耗了 80% 的執行時間。
- Donald Knuth 發現,不到 4% 的程式通常佔了 50% 以上的執行時間。
因此,應先量測找出熱點,再針對關鍵的少數百分比進行最佳化。
常見的錯誤觀念#
以下是關於程式碼調整的幾個迷思:
- 「減少高階語言的行數就能提升效能」——錯。 測試顯示,將迴圈展開為逐行賦值,反而比迴圈版本快 60% 以上。高階語言行數與機器碼效率之間沒有可預測的關係。
- 「某些操作大概比較快」——錯。 效能沒有「大概」的空間。語言、編譯器、版本、硬體任何一項改變都可能翻轉結論,必須實測。
- 「應該邊寫邊最佳化」——錯。 過早最佳化(Premature Optimization)是萬惡之源。程式完成前幾乎不可能正確辨識瓶頸,96% 的時間會浪費在不需要最佳化的程式碼上。
- 「快的程式跟正確的程式一樣重要」——錯。 正確性永遠優先。如果程式不需要正確運作,那讓它「瞬間完成」也毫無意義。
過早最佳化會傷害最終速度、程式品質、以及更重要的效能特性。先完成正確的程式,再讓它變快。
何時調整#
Jackson 的最佳化法則:
- 不要做。
- (僅限專家)還不要做。——等到你有一個完全清晰且未經最佳化的解決方案為止。
作者曾參與一個 C++ 圖表專案,初始繪圖耗時 45 分鐘。透過鎖定不到 1% 的程式碼,最終將時間從 45 分鐘降至約 1 秒。
編譯器最佳化#
現代編譯器的最佳化能力可能超乎預期。測試中,C++ 編譯器的最佳化可帶來高達 50-59% 的加速。但效果因編譯器而異——有些 Java VM 幾乎沒有改善。重要的是:最佳化編譯器對直觀的程式碼效果最好,「聰明的」程式碼反而會阻礙編譯器的最佳化。
25.3 常見的效能瓶頸#
程式碼調整的目標是找出程式中「又慢又肥」的部分,但你**必須靠剖析(Profiling)**才能確認——不能靠猜測。以下是歷來常見的效能殺手。
常見低效來源#
| 來源 | 說明 |
|---|---|
| 輸入/輸出操作(I/O) | 記憶體內存取比磁碟檔案快約 1000 倍,比網路存取更快。在速度關鍵的程式碼中應盡量避免 I/O |
| 分頁(Paging) | 觸發作業系統換頁的操作極慢。經典案例:二維陣列以錯誤的軸遍歷,修正迴圈順序後快了 1000 倍 |
| 系統呼叫(System Calls) | 涉及上下文切換,代價昂貴。作者案例中覆寫不必要的系統時間呼叫,效能改善超過其他所有變更的總和 |
| 直譯式語言 | PHP 與 Python 比 C++/C#/VB 慢 100 倍以上;Java 約慢 1.5 倍 |
| 程式錯誤(Errors) | 忘記關閉除錯日誌、未建資料庫索引等。加上遺漏的索引後效能改善了 30 倍 |
常見操作的相對成本#
點擊展開:操作成本摘要
大多數常見操作(常式呼叫、賦值、整數運算、浮點運算)的成本大致相當。值得注意的差異:
- 整數除法:比加法貴 5 倍(C++)
- 浮點除法:比加法貴 4 倍(C++)
- 超越函數(sqrt, sin, log, exp):比基本運算貴 15-50 倍
- 多型呼叫(Polymorphic Call):比一般呼叫略貴
程式碼調整的本質就是用廉價操作取代昂貴操作。
25.4 效能測量#
程式中的小部分通常消耗不成比例的執行時間。你必須量測才能找到熱點,然後再量測以驗證改善效果。
經驗在最佳化中幾乎無用——舊機器、舊語言、舊編譯器的經驗不適用於新環境。唯一能確定最佳化效果的方式就是量測。
作者曾將矩陣加總的陣列存取手動改為指標運算,預期能省下大量乘法運算。結果:毫無改善。反組譯後發現,編譯器早已自動將陣列存取轉換為指標——手動最佳化唯一的成果是讓程式碼更難讀。
量測的精確度#
- 使用**剖析工具(Profiling Tools)**或系統計時器,而非碼表。
- 量測 CPU 時脈週期,而非實際時間,以排除其他程式的干擾。
- 扣除量測本身的開銷與程式啟動開銷。
25.5 反覆調整#
一旦找到瓶頸,透過反覆套用多種技術可以獲得驚人的效能提升。單一技術很少能帶來 10 倍改善,但組合技術的累積效果可能非常可觀。
案例:DES 加密實作的 30 次迭代最佳化
作者的 DES 實作需在原始 IBM PC 上於 37 秒內加密 18K 檔案。初始版本耗時 21 分 40 秒。
| 最佳化步驟 | 時間 | 改善幅度 |
|---|---|---|
| 初始實作 | 21:40 | – |
| 位元欄位改為陣列 | 7:30 | 65% |
| 展開最內層迴圈 | 6:00 | 20% |
| 移除最終排列 | 5:24 | 10% |
| 合併兩個變數 | 5:06 | 5% |
| 利用邏輯恆等式合併前兩步 | 4:30 | 12% |
| 共享記憶體(內層迴圈) | 3:36 | 20% |
| 共享記憶體(外層迴圈) | 3:09 | 13% |
| 展開所有迴圈並使用字面索引 | 1:36 | 49% |
| 移除常式呼叫,全部內聯 | 0:45 | 53% |
| 改寫為組合語言 | 0:22 | 51% |
| 最終結果 | 0:22 | 98% |
最終程式碼是作者寫過最難讀、最難維護的程式。同時,至少三分之二嘗試過的最佳化並未成功。
25.6 程式碼調整方法總結#
程式碼調整的標準流程:
- 先用良好的設計開發——寫出易於理解與修改的程式碼。
- 效能不佳時:
- (a) 儲存目前可運作的版本(回退點)
- (b) 量測系統,找出熱點
- (c) 判斷瓶頸來自設計、資料型別還是演算法——若程式碼調整不適當,回到步驟 1
- (d) 調整步驟 (c) 找到的瓶頸
- (e) 逐一量測每項改善
- (f) 若改善無效,還原至步驟 (a) 的版本
- 從步驟 2 重複。
超過一半的調整嘗試只會帶來微不足道的改善或甚至使效能退化。永遠保留還原的能力。
flowchart TD
START["用良好設計開發程式碼"] --> CHECK{"效能不佳?"}
CHECK -->|否| END["完成"]
CHECK -->|是| APPROPRIATE{"程式碼調整是否適當?"}
APPROPRIATE -->|否| START
APPROPRIATE -->|是| SAVE["儲存目前可運作的版本"]
SAVE --> MEASURE["量測系統,找出熱點"]
MEASURE --> TUNE["針對瓶頸進行調整"]
TUNE --> VERIFY["量測調整效果"]
VERIFY --> IMPROVED{"效能改善?"}
IMPROVED -->|是| KEEP["保留變更"]
IMPROVED -->|否| REVERT["還原至先前版本"]
REVERT --> MEASURE
KEEP --> CHECK更多資源#
- Smith & Williams, Performance Solutions (2002)——涵蓋軟體效能工程,從開發各階段建立效能,包含 Web 應用與可擴展性的建議。
- Newcomer, “Optimization: Your Worst Enemy” (2000)——資深系統程式設計師對無效最佳化策略陷阱的生動描述。
- Knuth, The Art of Computer Programming vol. 1-3——演算法與資料結構的權威參考。
- Sedgewick, Algorithms in Java/C++/C——廣泛的演算法調查,涵蓋排序、搜尋、抽象資料型別與圖論。
要點#
- 效能只是軟體品質的一個面向,而且通常不是最重要的。程式架構、細部設計、資料結構與演算法選擇對效能的影響通常遠大於程式碼調整。
- 量化測量是效能最佳化的關鍵——用來找出真正值得改善的區域,也用來驗證最佳化確實帶來改善而非退化。
- 大多數程式把大部分時間花在一小部分程式碼上,在量測之前你不會知道是哪部分。
- 通常需要多次迭代才能透過程式碼調整達成預期的效能目標。
- 初始開發時為效能做的最好準備,是寫出乾淨、易理解、易修改的程式碼。