性能优化的一般策略及方法
性能優(yōu)化的一般策略及方法
在汽車嵌入式開發(fā)領域,性能優(yōu)化始終是一個無法回避的問題:
- 座艙 HMI 想要實現(xiàn)更流暢的人機交互
- 通信中間件在給定的 CPU 資源下,追求更高的吞吐量
- 更一般的場景:嵌入式設備 CPU 資源告急,需要降低 CPU 使用率...
不同的工程師會從不同的角度給出不同的優(yōu)化建議:
- 有人關注系統(tǒng)調用情況
- 有人建議從算法和數(shù)據(jù)結構入手
- 有人建議避免遞歸、循環(huán)嵌套
- 有人會從存儲器層次結構出發(fā),建議修改代碼提高緩存命中率來提升性能
- ...
這些都是具體的代碼調優(yōu)技術/技巧,或許有效,但不夠系統(tǒng)。本文不討論具體的代碼調優(yōu)技術,而是想介紹下具體代碼優(yōu)化技巧之上,更高層次的優(yōu)化策略。比起代碼級別的調優(yōu),可能效果更好,成本更低。
開始之前,需要強調下:
Premature optimization is the root of all evil. — Donald Knuth
一、性能概述
代碼調優(yōu)只是代碼性能優(yōu)化的方法之一,還有其他性能優(yōu)化的方法,也許效果更好、成本更低、對代碼的負面影響(降低可讀性/可維護性、引入 bug 等)也更少。
1.1 軟件質量和性能
性能只是眾多軟件質量標準中的一個。比起單純的代碼執(zhí)行速度,用戶可能更在意其他方面,比如穩(wěn)定可靠、簡潔易用等。
性能也不只是代碼的執(zhí)行速度,過分追求代碼的執(zhí)行速度而忽略其他方面可能會影響整體性能及軟件質量。
1.2 性能和代碼調優(yōu)
假如確定了把 Efficiency 作為首要目標,在代碼調優(yōu)之前,請優(yōu)先考慮:
- 性能需求
- 程序設計
- 類和方法設計
- 操作系統(tǒng)交互
- 編譯器優(yōu)化
- 硬件升級
- 代碼調優(yōu)
a. 性能需求
Barry Boehm 講過一個故事:某系統(tǒng)一開始要求亞秒級的響應時間,導致非常復雜的設計,預估成本 1 億美元。后來分析發(fā)現(xiàn),90%的情況下,用戶可以接受 4s 的響應時間。重新修改需求之后,節(jié)省了 7000 萬美元。
再舉一個例子,自動駕駛算法需要周期性獲取某些車輛數(shù)據(jù),當前的需求是 10ms 的周期上報。如果將周期改為 20ms 仍然可以滿足需求,那么不需要任何額外的優(yōu)化,CPU 占用率便可減少一半。
解決性能問題之前,先確認是否真的必要。
b. 程序設計
軟件架構設計主要如何將程序分解到模塊/類。有的設計決定了很難實現(xiàn)高性能,有的設計則容易實現(xiàn)高性能。
在軟件的架構設計中,設定資源占用的目標很重要:如果每個組件都能達成目標,則整個系統(tǒng)自然也可以。如果某個組件無法達成目標,也可以及早發(fā)現(xiàn),進行設計修改或代碼優(yōu)化。不僅如此,清晰的目標也更利于執(zhí)行和實施。
c. 類和方法設計
在程序設計基礎上更近一步,深入到類的內部。在這一層級,我們可以選擇數(shù)據(jù)結構和算法,從而影響程序的執(zhí)行速度和內存占用。
d. 操作系統(tǒng)交互
如果程序中涉及外部文件、動態(tài)內存、輸出設備,通常會和操作系統(tǒng)交互。如果程序性能不好,有可能就是系統(tǒng)調用過多導致的。有時系統(tǒng)庫或編譯器會在你意想不到的地方產(chǎn)生系統(tǒng)調用。
e. 編譯器
編譯器優(yōu)化比手工優(yōu)化代碼效果更好,也更安全!某種程度上來說,選擇了正確的編譯器,基本就不需要考慮代碼級優(yōu)化了。
f. 硬件
有時候升級硬件是解決性能問題成本最低的方案。不僅節(jié)省了性能優(yōu)化的人力成本,同時還避免了由于性能優(yōu)化引入的一系列隱性成本。同時,所有其他程序也因為硬件升級而得到性能提升。
g. 代碼調優(yōu)(Code Tuning)
“代碼調優(yōu)”指的是修改正確的代碼,使之運行得更快。代碼調優(yōu)的前提是代碼正確:設計良好,易于理解和修改。“調優(yōu)”指的是小規(guī)模修改,一個類,一個函數(shù)或者幾行代碼。“調優(yōu)”不包括大規(guī)模設計修改,以及更高層次的性能優(yōu)化手段。
上面從程序設計到代碼調優(yōu)六個層級中,每一個層級都可能產(chǎn)生 10 倍的性能提升,不同層級的組合起來理論上可以有百萬倍的提升。雖然實際不可能在每個層級都取得 10 倍的提升,但是這里想表達的是,性能優(yōu)化的空間潛力是巨大的。
二、代碼調優(yōu)
2.1 二八法則
a. 優(yōu)化哪里
有研究和報告表明:
- 20% 的函數(shù)占用了 80% 的程序執(zhí)行時間
- <4% 的代碼甚至能占用 50% 的執(zhí)行時間
不是每一行代碼都要做到最快,真正值得花時間把性能調到極致的代碼只有很小的一部分!
b. 誰來優(yōu)化
項目中系統(tǒng)整體的 CPU 接近滿負荷,其中 A 負責的模塊 CPU 占用 5%,而 B 負責的模塊 CPU 占用超過 60%。即便 A 再厲害,把自己優(yōu)化沒了,帶來的整體收益也不過 5%,而 B 卻因為有更大的優(yōu)化空間,能輕松地地降低 10%的 CPU 占用。
2.2 常見誤區(qū)
很多過時的、傳說中的代碼優(yōu)化技巧都是無效的,甚至能夠產(chǎn)生負面影響。
誤區(qū) 1: 代碼行數(shù)越少,程序越快
很容易找到一個反例:初始化大小為 N 的數(shù)組,直接寫出 N 條賦值語句,其性能是循環(huán)賦值的 2.5~4 倍!
誤區(qū) 2: xxxx 寫法很很可能更快
對于性能而言,沒有所謂的“很可能”,必須實際測量才知道到底是“優(yōu)化”了還是“劣化”了。影響性能的因素很多:處理器架構、編程語言、編譯器、編譯器版本、庫、庫的版本、內存大小...“很可能”是非常不負責任的說法,對于特定的環(huán)境是優(yōu)化,在另外環(huán)境下很能就是劣化。再次強調,必須要實際測量!
此外,為了“性能優(yōu)化”而引入的特殊寫法,反而會影響編譯器的優(yōu)化。
誤區(qū) 3: 從一開始就寫要出“快”的代碼
在程序沒最終完成之前,幾乎不可能識別出真正的性能瓶頸,你所“優(yōu)化”的代碼中,96%其實不需要優(yōu)化。過分關注執(zhí)行速度反而會影響軟件質量的其他方面。
Premature optimization is the root of all evil. — Donald Knuth
誤區(qū) 4: “快”和“正確”同等重要
如果程序不能正確運行,或者運行結果不正確,即使再快也沒有任何價值。
2.3 什么時候去調優(yōu)
Jackson's Rule of Optimization:
Rule 1. Don't do it.
Rule 2 (for expert only). Don't do it yet -- that is, not until you have a perfectly clear and unoptimized solution.
簡言之,非必要,不優(yōu)化。先保證良好的設計,編寫易于理解和修改的整潔代碼。如果現(xiàn)有的代碼很糟糕,先清理重構,然后再考慮優(yōu)化。
2.4 編譯器優(yōu)化
現(xiàn)代編譯器優(yōu)化遠比你想象中的更強大。例如編譯器能夠識別并優(yōu)化循環(huán)嵌套,比手動優(yōu)化更安全,效果也更好。不要自作聰明地用一些幾十年前所謂的特殊“優(yōu)化技巧”,大概率會給編譯器造成困擾,適得其反。
-
各家的編譯器各有優(yōu)缺點,選擇最適合項目的編譯器
-
開啟編譯器的不同優(yōu)化選項,性能可提升為原來的 2 倍甚至更多
程序員應該專注于寫整潔代碼(設計良好,意圖明確清晰,可讀性好,易于維護),優(yōu)化的事情交給編譯器就好啦!
三、導致性能問題的常見原因
3.1 常見性能問題元兇
a. 輸入/輸出操作
不必要的 I/O 操作是最常見的導致性能問題的罪魁禍首。比如頻繁讀寫磁盤上的文件、通過網(wǎng)絡訪問數(shù)據(jù)庫等。一般來說,內存的讀寫性能是磁盤的幾千幾萬倍,如果有內存不是很 critical,可以將數(shù)據(jù)保存在內存中以減少不必要的 IO 操作從而改善性能。
幾年前在一個基于 Qt 的座艙項目中,從 CarPlay 界面返回車機首頁會有短暫的卡頓,導致無法通過 CarPlay 的認證。用 QmlProfiler 分析發(fā)現(xiàn),切換卡頓是由于從磁盤加載背景圖片導致的,將背景圖片緩存在內存中,可以直接消除圖片加載時間,大幅提升界面切換的流暢度。代價是犧牲了一定的內存,這是一個空間換時間的典型例子。
b. 缺頁
有一個經(jīng)典的例子:
// BAD
for (int col = 0; col < MAX_COLUMNS; ++col) {
for(int row = 0; row < MAX_ROWS; ++row) {
table[row][col] = GetDefaultValue();
}
}
// GOOD
for (int row = 0; row < MAX_ROWS; ++row) {
for(int col = 0; col < MAX_COLUMNS; ++col) {
table[row][col] = GetDefaultValue();
}
}
以上兩種寫法在特定場景下,性能差距可達 1000 倍。背后涉及到二維數(shù)組在內存中的存儲方式以及緩存命中等知識,CSAPP 的第 5、6 章對此有詳細闡述。
c. 系統(tǒng)調用
系統(tǒng)調用需要進行上下文切換,保存程序狀態(tài)、恢復內核狀態(tài)等一些步驟,開銷相對較大。對磁盤的讀寫操作、對鍵盤、屏幕等外設的操作、內存管理函數(shù)的調用等都屬于系統(tǒng)調用。
Linux 系統(tǒng)調用可以通過 strace 查看,qnx 也有 tracelogger 等工具
d. 解釋型語言
一般來說,C/C++/VB/C# 這種編譯型語言的性能好于 Java 的字節(jié)碼,好于 PHP/Pyhon 等解釋型語言。這也是為什么汽車嵌入式領域還是 C/C++ 天下等主要原因。
e. 錯誤
還有很大很一部分導致性能問題的原因可以歸為錯誤:忘了把調試代碼(如保存 trace 到文件)關閉,忘記釋放資源/內存泄漏、數(shù)據(jù)庫表設計缺陷(常用表沒有索引)等。
3.2 常見操作的相對開銷
| 操作 | 示例 | 相對耗時(C++) |
|---|---|---|
| 整數(shù)賦值(基準) | i = j |
1 |
| 函數(shù)調用 | ||
| 普通函數(shù)調用(無參) | foo() |
1 |
| 普通函數(shù)調用(單參) | foo(i) |
1.5 |
| 普通函數(shù)調用(雙參) | foo(i,j) |
2 |
| 類的成員函數(shù)調用 | bar.foo() |
2 |
| 子類的成員函數(shù)調用 |
derivedBar.foo() |
2 |
| 多態(tài)方法調用 | abstractBar.foo() |
2.5 |
| 對象解引用 | ||
| 訪問對象成員(一級/二級) | i = obj1.obj2.num |
1 |
| 整數(shù)運算 | ||
| 整數(shù)賦值/加/減/乘 | i = j * k |
1 |
| 整數(shù)除法 | i = j / k |
5 |
| 浮點運算 | ||
| 浮點賦值/加/減/乘 | x = y * z |
1 |
| 浮點除法 | x = y / z |
4 |
| 超越函數(shù) | ||
| 浮點根號 | y = sqrt(x) |
15 |
| 浮點 sin | y = sin(x) |
25 |
| 浮點對數(shù) | y = log(x) |
25 |
| 浮點指數(shù) | y = exp(x) |
50 |
| 數(shù)組操作 | ||
| 一維/二維整數(shù)/浮點數(shù)組下標訪問 | x = a[3][j] |
1 |
注:上表僅供參考,不同處理器、不同語言、不同編譯器、不同測試環(huán)境所得結果可能相差很大!
代碼調優(yōu)的方式之一就是用低開銷的操作替代高開銷操作。一般操作(賦值、函數(shù)調用、算數(shù)運算)的開銷基本相同,除法運算開銷較大,超越函數(shù)開銷尤其巨大,多態(tài)函數(shù)的調用較普通函數(shù)調用有一定額外開銷。
四、測量
代碼執(zhí)行耗時和代碼量不成比例,必須經(jīng)過測量才知道時間花在哪里。找到問題,優(yōu)化,重新測量。
性能優(yōu)化很多時候是反直覺的(比如代碼量越少不一定越快),只有測量了才知道是否有效果。
過往的經(jīng)驗可能不會有太多幫助,針對舊的機器、語言、編譯器的優(yōu)化經(jīng)驗在現(xiàn)在可能完全不適用,必須要實際測量了才知道!
比如在舊版本的編譯器中,把二維數(shù)組的操作轉為對單個指針操作可以提升性能,而在新的編譯器卻完全沒有效果,因為新版編譯器會自動進行這樣的轉化。而手動修改代碼只會降低代碼的可讀性。
測量要準確
- 用專門的 Profiling 工具或者系統(tǒng)時間
- 只測量你自己的代碼部分
- 必要時需要用 CPU 時鐘 tick 數(shù)來替代時間戳以獲得更準確的測量結果
要想準確的測量是一件非常困難的事情。不同的硬件、進程的優(yōu)先級、線程調度策略、測量時其他的進程的運行、甚至外界環(huán)境都可能對測量結果產(chǎn)生影響。我們能做的就是盡可能地控制變量,剔除無關因素影響。
五、迭代
很難只用一個技巧就把性能提升 10 倍,但是可以不斷嘗試,組合不同技巧,最終實現(xiàn)巨大的性能提升。下面是一個通過不斷迭代優(yōu)化,將執(zhí)行時間從 21 分 40 秒優(yōu)化到 22 秒的例子:
| 優(yōu)化項 | 執(zhí)行時間 |
|---|---|
| 初版,直接實現(xiàn) | 21:40 |
| bit 轉數(shù)組 | 7:30 |
| 展開最內層 for 循環(huán) | 6:00 |
| 去除最終排列 | 5:24 |
| 合并 2 個變量 | 5:06 |
| 合并算法的前兩步 | 4:30 |
| 在內層循環(huán)中,使兩個變量共享同一內存 | 3:36 |
| 在外層循環(huán)中,使兩個變量共享同一內存 | 3:09 |
| 展開所有循環(huán),使用字面量下標 | 1:36 |
| 去除所有函數(shù)調用,把代碼寫在一行 | 0:45 |
| 用匯編重寫整個函數(shù) | 0:22 |
六、調優(yōu)一般方法
- 程序設計良好,易于理解和修改(前提)
- 如果性能不佳:
a. 保存當前狀態(tài)
b. 測量,找出時間主要消耗在哪里
c. 分析問題:是否因為高層設計、數(shù)據(jù)結構、算法導致的,如果是,返回步驟 1
d. 如果設計、數(shù)據(jù)結構、算法沒問題,針對上述步驟中的瓶頸進行代碼調優(yōu)
e. 每進行一項優(yōu)化,立即進行測量
f. 如果沒有效果,恢復到 a 的狀態(tài)。(大多數(shù)的調優(yōu)嘗試幾乎不會對性能產(chǎn)生影響,甚至產(chǎn)生負面影響。代碼調優(yōu)的前提是代碼設計良好,易于理解和修改。Code tuning 通常會對設計、可讀性、可維護性產(chǎn)生負面影響,如果 tuning 改良了設計或者可讀性,那么不應該叫 tuning,而是屬于步驟 1) - 重復步驟 2
七、總結
- 性能只是眾多軟件質量指標中的一個,而且一般不是最重要的那個。精心調優(yōu)之后的代碼也只能對整體性能產(chǎn)生部分影響,程序架構、詳細設計、數(shù)據(jù)結構/算法的選擇、編譯器通常比代碼本身對性能的影響更大。
-
準確地測量至關重要
- 絕大多數(shù)程序的大部分時間都耗在少數(shù)代碼上,只有測量了才知道時間花在了哪里,優(yōu)化重點在哪里
- 很多“優(yōu)化技巧”實際上不僅不會提高性能,甚至會降低性能,只有測量了才能知道
- 測量越接近真實環(huán)境越好,模擬的測試環(huán)境和程序實際運行環(huán)境可能得到完全不同的結果!
- 通常需要多輪優(yōu)化迭代才能達到預期性能目標
- 如果想為今后(可能)的性能優(yōu)化提前作準備,最好的準備就是編寫易于理解和修改的整潔代碼
7.1 檢查清單
- 明確需求,是否真的有這么高的性能要求?
- 嘗試提高編譯器優(yōu)化選項?
- 考慮升級/更換編譯器?
- 考慮過升級/更換硬件?
- 程序的 high-level design、類設計是否合理?
- 檢查是否有不必要的系統(tǒng)調用、I/O 操作?
- 考慮用編譯型語言替代解釋型語言?
- 代碼調優(yōu)是否作為最后手段?
7.2 代碼調優(yōu)方法
- 調優(yōu)的前提:代碼正確,設計良好,易于理解和修改
- 測量,找出瓶頸
- 每次優(yōu)化后,立即重新測量
- 如果沒有效果,撤銷改動
- 嘗試多種方法,不斷迭代
八、擴展閱讀
- 《CSAPP》第 5、6 章
- 《Code Complete》第 25、26 章
- 《C++ Core Guidelines》Per 章節(jié)
總結
以上是生活随笔為你收集整理的性能优化的一般策略及方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【从零开始的ROS四轴机械臂控制】(七)
- 下一篇: Django笔记四十三之使用uWSGI部