聊聊軟件性能優(yōu)化全景
本文轉載自微信公眾號「碼磚雜役」,作者我不想種地。轉載本文請聯(lián)系碼磚雜役公眾號。
性能優(yōu)化是指在不影響正確性的前提下,使程序運行得更快,它是一個非常廣泛的話題。
軟件產品多種多樣,影響程序執(zhí)行效率的因素很多,因此,性能優(yōu)化,特別是對不熟悉的項目做優(yōu)化,不是一件容易的事。
性能優(yōu)化可分為宏觀和微觀兩個層面。宏觀層面包括架構重構,而微觀層面,則包括算法的優(yōu)化,編譯優(yōu)化,工具分析,高性能編碼等,這些方法是有可能獨立于具體業(yè)務邏輯,因而有更加廣泛的適應性,且更易于實施。
具體到性能優(yōu)化的方法論,首先,應建立度量,你度量什么,你得到什么。所以,性能優(yōu)化測試先行,須基于數(shù)據(jù)而不能憑空猜測,這是做優(yōu)化的一個基本原則。搭建真實的壓測環(huán)境,或者逼近真實環(huán)境,有時候是困難的,也可能非常耗費時間,但它依然是值得的。
有許多工具能幫助我們定位程序瓶頸,有些工具能做很友好的圖形化展示,定位問題是解決問題的前置條件,但定位問題可能不是最難的,分析和優(yōu)化才是最耗時的關鍵環(huán)節(jié),修改之后,要再回歸測試,驗證是否如預期般有效。
什么是高性能程序?架構致廣遠、實現(xiàn)盡精微。
架構優(yōu)化的關鍵是識別瓶頸,這類優(yōu)化有很多套路,比如通過負載均衡做分布式改造,比如用多線程協(xié)程做并行化改造,比如用消息隊列做異步化和解耦,比如用事件通知替代輪詢,比如為數(shù)據(jù)訪問增加緩存,比如用批處理+預取提升吞吐,比如IO與邏輯分離、讀寫分離等等。
架構調整和優(yōu)化雖然收效很大,卻因受限于各種現(xiàn)實因素,因而并不總是可行。
能不做的盡量不做、必須做的高效做是性能優(yōu)化的一個根本法則,提升處理能力和降低計算量可視為性能優(yōu)化的兩個方向。
怎么讓程序跑的更快?這要求我們充分利用硬件的各種特性,想方設法減少等待并且提高并發(fā),提升CACHE命中率,使用更高效的結構和算法;而降低計算量,則可能意味著要跳出純技術范疇,從產品和業(yè)務視角去審視:哪些功能是必須的,哪些功能是可選可配置的。
有時候,我們不得不從細節(jié)的維度去改進程序。通常,我們應該使用簡單的數(shù)據(jù)結構和算法,但如有必要,就應積極使用更高效的結構和算法,不止邏輯結構,物理結構(實現(xiàn))同樣影響執(zhí)行效率;分支預測、反饋優(yōu)化、啟發(fā)性以及基于機器學習編譯優(yōu)化的效果日益凸顯;熟練掌握編程語言深刻理解標準庫實現(xiàn)能幫助我們規(guī)避低性能陷阱;深入細節(jié)做代碼微調甚至指令級優(yōu)化有時候也能取得意想不到的效果。
有時候,我們需要做一些交換,比如用空間置換時間,比如犧牲一些通用性可讀性換取高性能,我們只應當在非常必要的情況下才這么做,它是權衡的藝術。
## 1、架構優(yōu)化
### 負載均衡
負載均衡其實就是解決一個分活的問題,對應到分布式系統(tǒng),一般在邏輯服的前面都會安放一個負載均衡器,比如NGINX就是經典的解決方案。負載均衡不限于分布式系統(tǒng),對于多線程架構的服務器內部,也需要解決負載均衡的問題,讓各個worker線程的負載均衡。
### 多線程、協(xié)程并行化
雖然硬件架構的復雜化對程序開發(fā)提出了更高的要求,但編寫充分利用多CPU多核特性的程序能獲得令人驚嘆的收益,所以,在同樣硬件規(guī)格下,基于多線程/協(xié)程的并行化改造依然值得嘗試。
多線程不可避免要面臨資源競爭的問題,我們的設計目標應該是充分利用硬件多執(zhí)行核心的優(yōu)勢,減少等待,讓多個執(zhí)行流暢快的奔跑起來。
對于多線程模型,如果把每一個要干的活抽象為一個task,把干活的線程抽象為worker,那么,有兩種典型的設計思路,一種是對task類型做出劃分,讓一類或者一個worker去干特定的task,另一種是讓所有worker去干所有task。
第一種劃分,能減少數(shù)據(jù)爭用,編碼實現(xiàn)也更簡單,只需要識別有限的競爭,就能讓系統(tǒng)工作的很好,缺點是任務的工作量很可能不同,有可能導致有些worker忙碌而另一些空閑。
第二種劃分,優(yōu)點是能均衡,缺點是編碼復雜性高,數(shù)據(jù)競爭多。
有時候,我們會綜合上述兩種模式,比如讓單獨的線程去做IO(收發(fā)包)+反序列化(產生protocol task),然后啟動一批worker線程去處理包,中間通過一個task queue去連接,這即是經典的生產者消費者模型。
協(xié)程是一種用戶態(tài)的多執(zhí)行流,它基于一個假設,即用戶態(tài)的任務切換成本低于系統(tǒng)的線程切換。
### 通知替代輪詢
輪詢即不停詢問,就像你每隔幾分鐘去一趟宿管那里查看是否有信件,而通知是你告訴宿管阿姨,你有信的時候,她打電話通知你,顯然輪詢耗費CPU,而通知機制效率更高。
### 添加緩存
緩存的理論依據(jù)是局部性原理。
一般系統(tǒng)的寫入請求遠少于讀請求,針對寫少讀多的場景,很適合引入緩存集群。
在寫數(shù)據(jù)庫的時候同時寫一份數(shù)據(jù)到緩存集群里,然后用緩存集群來承載大部分的讀請求,因為緩存集群很容易做到高性能,所以,這樣的話,通過緩存集群,就可以用更少的機器資源承載更高的并發(fā)。
緩存的命中率一般能做到很高,而且速度很快,處理能力也強(單機很容易做到幾萬并發(fā)),是理想的解決方案。
CDN本質上就是緩存,被用戶大量訪問的靜態(tài)資源緩存在CDN中是目前的通用做法。
### 消息隊列
消息隊列、消息中間件是用來做寫請求異步化,我們把數(shù)據(jù)寫入MessageQueue就認為寫入完成,由MQ去緩慢的寫入DB,它能起到削峰填谷的效果。
消息隊列也是解耦的手段,它主要用來解決寫的壓力。
### IO與邏輯分離、讀寫分離
IO與邏輯分離,這個前面已經講了。讀寫分離是一種數(shù)據(jù)庫應對壓力的慣用措施,當然,它也不僅限于DB。
### 批處理與數(shù)據(jù)預取
批處理是一種思想,分很多種應用,比如多網(wǎng)絡包的批處理,是指把收到的包攢到一起,然后一起過一遍流程,這樣,一個函數(shù)被多次調用,或者一段代碼重復執(zhí)行多遍,這樣i-cache的局部性就很好,另外,如果這個函數(shù)或者一段里要訪問的數(shù)據(jù)被多次訪問,d-cache的局部性也能改善,自然能提升性能,批處理能增加吞吐,但通常會增大延遲。
另一個批處理思想的應用是日志落盤,比如一條日志大概寫幾十個字節(jié),我們可以把它緩存起來,攢夠了一次寫到磁盤,這樣性能會更好,但這也帶來數(shù)據(jù)丟失的風險,不過通常我們可以通過shm的方式規(guī)避這個風險。
指令預取是CPU自動完成的,數(shù)據(jù)預取是一個很有技巧性的工作,數(shù)據(jù)預取的依據(jù)是預取的數(shù)據(jù)將在接下來的操作中用到,它符合空間局部性原理,數(shù)據(jù)預取可以填充流水線,降低訪存等待,但數(shù)據(jù)預取會侵害代碼,且并不總如預期般有效。
哪怕你不增加預取代碼,硬件預取器也有可能幫你做預取,另外gcc也有編譯選項,開啟它會在編譯階段自動插入預取代碼,手動增加預取代碼需要小心處理,時機很重要,最后一定要基于測試數(shù)據(jù),另外,即使表現(xiàn)很好,但代碼修改也有可能導致效果衰減,而且預取語句執(zhí)行本身也有開銷,只有預取的收益大于預取的開銷才是值得的。
累啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦啦,寫不動啦啦啦!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!哪天有空再寫后面的章節(jié)吧
## 2、算法優(yōu)化
### 哈希(HASH)
#### 哈希和字符串比較
#### HashMap
#### 哈希和平衡搜索樹的比較
### 基于有序數(shù)組的二分查找
### 數(shù)據(jù)結構的實現(xiàn)優(yōu)化
### 延遲計算 & 寫時拷貝
### 預計算
### 增量更新
## 3、代碼優(yōu)化
### 內存優(yōu)化
小對象分配器
內存分配和對象構建分離
### cache優(yōu)化
i-cache優(yōu)化、d-cache優(yōu)化、cache對齊、結構體重排
### 判斷前置
### 整體操作替代小操作
### 復用
### 減法
#### 減少冗余
#### 減少拷貝、零拷貝
#### 減少參數(shù)個數(shù)(寄存器參數(shù)、取決于ABI約定)
#### 減少函數(shù)調用次數(shù)/層次
#### 減少存儲引用次數(shù)
#### 減少無效初始化和重復賦值
### 循環(huán)優(yōu)化
### 防御性編程適可而止
### release干凈
### 慎用遞歸
## 4、編譯優(yōu)化
### inline
### restrict
### LTO
### PGO
### 優(yōu)化選項
## 5、其他優(yōu)化
### 綁核
### SIMD
### 鎖與并發(fā)
#### 鎖的粒度
#### 無鎖編程
#### Per-cpu data structure & thread local
#### 內存屏障
小結
性能優(yōu)化是一項細致的工作,工程師們曾致力于尋找一勞永逸解決性能問題的捷徑,但遺憾的是,沒有銀彈,但這并不意味著性能優(yōu)化無章可循。軟件工程師們在性能優(yōu)化方面積累了大量的經驗,包括架構、緩存、預取、工具、編譯器與編程語言,代碼重構等實踐經驗方方面面,這些方法和探討都具有借鑒意義。
性能優(yōu)化也是一個系統(tǒng)性工程,出現(xiàn)性能瓶頸再優(yōu)化是一種先污染后治理的思路。更好的方式是將性能貫穿于軟件的整個生命周期之中,在設計之初即把性能作為一項需求甚至關鍵目標加以考慮,開發(fā)中持續(xù)監(jiān)控性能的變化并嚴格遵從高性能編碼規(guī)范,后期維護將性能納入維護體系。
嚴格的說,性能優(yōu)化和性能設計有所不同,性能優(yōu)化通常是在現(xiàn)有系統(tǒng)和代碼基礎上做改進,它并非推倒重來,考驗的是開發(fā)者反向修復的能力,而性能設計考驗的是設計者的正向設計能力,但性能優(yōu)化的方法可以指導性能設計,兩者互補。