seo常用優化技巧「網站代碼優化工具」

性能優化是指在不影響正確性的前提下,使程序運行得更快,它是一個非常廣泛的話題。

優化有時候是為了降低成本,但有時候,性能能決定一個產品的成敗,比如遊戲服務器的團戰玩法需要單服達到一定的同時在線人數才能支撐起這類玩法,而電信軟件的性能往往是競標的核心競爭力,性能關乎商業成敗。

軟件產品多種多樣,影響程序執行效率的因素很多,因此,性能優化,特別是對不熟悉的項目做優化,不是一件容易的事。

性能優化可分為宏觀和微觀兩個層面。宏觀層面包括架構重構,而微觀層面,則包括算法的優化,編譯優化,工具分析,高性能編碼等,這些方法是有可能獨立於具體業務邏輯,因而有更加廣泛的適應性,且更易於實施。

具體到性能優化的方法論,首先,應建立度量,你度量什麼,你得到什麼。所以,性能優化測試先行,須基於數據而不能憑空猜測,這是做優化的一個基本原則。搭建真實的壓測環境,或者逼近真實環境,有時候是困難的,也可能非常耗費時間,但它依然是值得的。

有許多工具能幫助我們定位程序瓶頸,有些工具能做很友好的圖形化展示,定位問題是解決問題的前置條件,但定位問題可能不是最難的,分析和優化才是最耗時的關鍵環節,修改之後,要再回歸測試,驗證是否如預期般有效。

什麼是高性能程序?架構致廣遠、實現盡精微。

架構優化的關鍵是識別瓶頸,這類優化有很多套路,比如通過負載均衡做分布式改造,比如用多線程協程做並行化改造,比如用消息隊列做異步化和解耦,比如用事件通知替代輪詢,比如為數據訪問增加緩存,比如用批處理+預取提升吞吐,比如IO與邏輯分離、讀寫分離等等。

架構調整和優化雖然收效很大,卻因受限於各種現實因素,因而並不總是可行。

能不做的盡量不做、必須做的高效做是性能優化的一個根本法則,提升處理能力和降低計算量可視為性能優化的兩個方向。

怎麼讓程序跑的更快?這要求我們充分利用硬件的各種特性,想方設法減少等待並且提高並發,提升CACHE命中率,使用更高效的結構和算法;而降低計算量,則可能意味着要跳出純技術範疇,從產品和業務視角去審視:哪些功能是必須的,哪些功能是可選可配置的。

有時候,我們不得不從細節的維度去改進程序。通常,我們應該使用簡單的數據結構和算法,但如有必要,就應積極使用更高效的結構和算法,不止邏輯結構,物理結構(實現)同樣影響執行效率;分支預測、反饋優化、啟發性以及基於機器學習編譯優化的效果日益凸顯;熟練掌握編程語言深刻理解標準庫實現能幫助我們規避低性能陷阱;深入細節做代碼微調甚至指令級優化有時候也能取得意想不到的效果。

有時候,我們需要做一些交換,比如用空間置換時間,比如犧牲一些通用性可讀性換取高性能,我們只應當在非常必要的情況下才這麼做,它是權衡的藝術。

工具人一文講清軟件性能優化

## 1、架構優化

通常系統的throughput越大,latency就越高,但過高的latency不可接受,所以架構優化不是一味追求throughput,也需要關注latency,追求可接受latency下的高throughput。

### 負載均衡

負載均衡其實就是解決一個分活的問題,對應到分布式系統,一般在邏輯服的前面都會安放一個負載均衡器,比如NGINX就是經典的解決方案。負載均衡不限於分布式系統,對於多線程架構的服務器內部,也需要解決負載均衡的問題,讓各個worker線程的負載均衡。

### 多線程、協程並行化

雖然硬件架構的複雜化對程序開發提出了更高的要求,但編寫充分利用多CPU多核特性的程序能獲得令人驚嘆的收益,所以,在同樣硬件規格下,基於多線程/協程的並行化改造依然值得嘗試。

多線程不可避免要面臨資源競爭的問題,我們的設計目標應該是充分利用硬件多執行核心的優勢,減少等待,讓多個執行流暢快的奔跑起來。

對於多線程模型,如果把每一個要乾的活抽象為一個task,把幹活的線程抽象為worker,那麼,有兩種典型的設計思路,一種是對task類型做出劃分,讓一類或者一個worker去干特定的task,另一種是讓所有worker去干所有task。

第一種劃分,能減少數據爭用,編碼實現也更簡單,只需要識別有限的競爭,就能讓系統工作的很好,缺點是任務的工作量很可能不同,有可能導致有些worker忙碌而另一些空閑。

第二種劃分,優點是能均衡,缺點是編碼複雜性高,數據競爭多。

有時候,我們會綜合上述兩種模式,比如讓單獨的線程去做IO(收發包)+反序列化(產生protocol task),然後啟動一批worker線程去處理包,中間通過一個task queue去連接,這即是經典的生產者消費者模型。

協程是一種用戶態的多執行流,它基於一個假設,即用戶態的任務切換成本低於系統的線程切換。

### 通知替代輪詢

輪詢即不停詢問,就像你每隔幾分鐘去一趟宿管那裡查看是否有信件,而通知是你告訴宿管阿姨,你有信的時候,她打電話通知你,顯然輪詢耗費CPU,而通知機制效率更高。

### 添加緩存

緩存的理論依據是局部性原理。

一般系統的寫入請求遠少於讀請求,針對寫少讀多的場景,很適合引入緩存集群。

在寫數據庫的時候同時寫一份數據到緩存集群里,然後用緩存集群來承載大部分的讀請求,因為緩存集群很容易做到高性能,所以,這樣的話,通過緩存集群,就可以用更少的機器資源承載更高的並發。

緩存的命中率一般能做到很高,而且速度很快,處理能力也強(單機很容易做到幾萬並發),是理想的解決方案。

CDN本質上就是緩存,被用戶大量訪問的靜態資源緩存在CDN中是目前的通用做法。

### 消息隊列

消息隊列、消息中間件是用來做寫請求異步化,我們把數據寫入MessageQueue就認為寫入完成,由MQ去緩慢的寫入DB,它能起到削峰填谷的效果。

消息隊列也是解耦的手段,它主要用來解決寫的壓力。

### IO與邏輯分離、讀寫分離

IO與邏輯分離,這個前面已經講了。讀寫分離是一種數據庫應對壓力的慣用措施,當然,它也不僅限於DB。

### 批處理與數據預取

批處理是一種思想,分很多種應用,比如多網絡包的批處理,是指把收到的包攢到一起,然後一起過一遍流程,這樣,一個函數被多次調用,或者一段代碼重複執行多遍,這樣i-cache的局部性就很好,另外,如果這個函數或者一段里要訪問的數據被多次訪問,d-cache的局部性也能改善,自然能提升性能,批處理能增加吞吐,但通常會增大延遲。

另一個批處理思想的應用是日誌落盤,比如一條日誌大概寫幾十個字節,我們可以把它緩存起來,攢夠了一次寫到磁盤,這樣性能會更好,但這也帶來數據丟失的風險,不過通常我們可以通過shm的方式規避這個風險。

指令預取是CPU自動完成的,數據預取是一個很有技巧性的工作,數據預取的依據是預取的數據將在接下來的操作中用到,它符合空間局部性原理,數據預取可以填充流水線,降低訪存等待,但數據預取會侵害代碼,且並不總如預期般有效。

哪怕你不增加預取代碼,硬件預取器也有可能幫你做預取,另外gcc也有編譯選項,開啟它會在編譯階段自動插入預取代碼,手動增加預取代碼需要小心處理,時機的選擇很重要,最後一定要基於測試數據,另外,即使預取表現很好,但代碼修改也有可能導致效果衰減,而且預取語句執行本身也有開銷,只有預取的收益大於預取的開銷,且CACHE-MISS很高才是值得的。

## 2、算法優化

數據量小的集合上遍歷查找即可,但如果循環的次數過百,便需要考慮用更快的查找結構和算法替換蠻力遍歷,哈希表,紅黑樹,二分查找很常用。

### 哈希(HASH)

哈希也叫散列,是把任意長度的輸入通過散列算法變換成固定長度的輸出,該輸出就是散列值,也叫摘要。比如把一篇文章的內容通過散列生成64位的摘要,該過程不可逆。

這種轉換是一種壓縮映射,也就是,散列值的空間通常遠小於輸入的空間,不同的輸入可能會散列成相同的輸出,所以不可能從散列值來確定唯一的輸入值,但如果輸出的位數足夠,散列成相同輸出的概率非常非常小。

字符串的比較有時會成為消耗較大的操作,雖然strcmp或者memcpy的實現用到了很多加速和優化技巧,但本質上它還是逐個比較的方式。

字符串比較的一個改進方案就是哈希,比較哈希值(通常是一個int64的整數)而非比較內容能快很多,但需要為字符串提前計算好哈希值,且需要額外的空間保存哈希值,另外,在哈希值相等的時候,還需要比較字符串,但因為衝突的概率極低,所以後續的字符串比較不會很多次。

這樣不一定總是更高效,但它提供了另一個思路,你需要測試你的程序,再決定要不要這樣做。

另一個哈希的用法是哈希表,哈希表的經典實現是提前開闢一些桶,通過哈希找到元素所在的桶(編號),如果衝突,再拉鏈解決衝突。

為了減少衝突經常需要開闢更多的桶,但更多的桶需要更大的存儲空間,特別是元素數量不確定的時候,桶的數量選擇變得兩難,隨着裝載的元素變多,衝突加劇,在擴容的時候,將需要對已存在的元素重新哈希,這是很費的點。

哈希表的衝突極端情況下會退化成鏈表,當初設想的快速查找變得不再可行,HashMap是普通哈希表的改進版,結合哈希和二叉平衡搜索樹。

另一個常用來做查找的結構是紅黑樹,它能確保最壞情況下,有logN的時間複雜度,但紅黑樹的查找過程需要沿着鏈走,不同結點內存通常不連續,CACHE命中性經常很差,紅黑樹的中序遍歷結果是有序的,這是哈希表不具備的,另外,紅黑樹不存在哈希表那般預估容量難的問題。

### 基於有序數組的二分查找

二分查找的時間複雜度也是logN,跟紅黑樹一致,但二分查找的空間局部性更好,不過二分查找有約束,它只能在有序數組上進行,所以,如果你需要在固定的數據集合(比如配置數據)做查找,二分查找是個不錯的選擇。

### 跳錶(Skip List)

跳錶增加了向前指針,是一種多層結構的有序鏈表,插入一個值時有一定概率晉陞到上層形成間接的索引。

跳錶是一個隨機化的數據結構,實質就是一種可以進行二分查找的有序鏈表。跳錶在原有的有序鏈表上面增加了多級索引,通過索引來實現快速查找。跳錶不僅能提高搜索性能,同時也可以提高插入和刪除操作的性能。

跳錶適合大量並發寫的場景,可以認為是隨機平衡的二叉搜索樹,不存在紅黑樹的再平衡問題。Redis強大的ZSet底層數據結構就是哈希加跳錶。

相比哈希表和紅黑樹,跳錶用的不那麼多。

### 數據結構的實現優化

我們通常只會講數據的邏輯結構,但數據的實現(存儲)結構也會影響性能。

數組在存儲上一定是邏輯地址連續的,但鏈表不具有這樣的特點,鏈表通過鏈域尋找臨近節點,如果相鄰節點在地址上發散,則沿着鏈域訪問效率不高,所以實現上可以通過從單獨的內存配置器分配結點(盡量內存收斂)來優化訪問效率,同樣的方法也適應紅黑樹、哈希表等其他結構。

### 排序

盡量對指針、索引、ID排序,而不要對對象本身排序,因為交換對象比交換地址/索引慢;求topN不要做全排序;非穩定排序能滿足要求不要搞穩定排序。

### 延遲計算 & 寫時拷貝

延遲計算和寫時拷貝(COW)思想上是一樣的,即可以通過把計算盡量推遲來減少計算開銷。

我拿遊戲服務器開發來舉例,假設玩家的戰鬥力(fight)是通過等級,血量,稱號等其他屬性計算出來的,我們可以在等級、血量、稱號變化的時候立即重算fight,但血量可能變化比較頻繁,所以就會需要頻繁重算戰力。通過延遲計算,我們可以為戰力添加一個dirtyFlag,在等級、血量、稱號變化的時候設置dirtyFlag,等到真正需要用到戰力的時候(GetFight函數)里判斷dirtyFlag,如果dirtyFlag為true則重算戰力並清dirtyFlag,如果dirtyFlag為false則直接返回fight值。

寫時拷貝(COW)跟這個差不多,linux kernel在fork進程的時候,子進程會共享父進程的地址空間,只有在子進程對自身地址空間寫的時候,才會clone一份出來,同樣,string的設計也用到了類似的思想。

### 預計算

有些值可以提前計算出結果並保存起來,不用重複計算的盡量不重複計算,特別是循環內的計算,要避免重複的常量計算,C++甚至增加了一個constexpr的關鍵詞。

### 增量更新

增量更新的原理不複雜,只做增量,只做DIFF,不做全量,這個思想有很多應用場景。

舉個例子,遊戲服務器每隔一段時間需要把玩家的屬性(比如血量、魔法值等)同步到客戶端,簡單的做法是把所有屬性打包一次性全發送過去,這樣比較耗費帶寬,可以考慮為每個屬性編號,在發送的時候,只發送變化的屬性。

在發送端,編碼一個變化的屬性的時候,需要發送一個屬性編號+屬性值的對子,接收端類似,先解出屬性編號,再解出屬性值,這種方式可能需要犧牲一點CPU換帶寬。

## 3、代碼優化

### 內存優化

(a)小對象分配器

C的動態內存分配是介於系統和應用程序的中間層,malloc/free本身體現的就是一種按需分配+復用的思想。

當你調用malloc向glibc的動態內存分配器ptmalloc申請6字節的內存,實際耗費的會大於6字節,6是動態分配塊的有效載荷,動態內存分配器會為chunk添加首部和尾部,有時候還會加一下填充,所以,真正耗費的存儲空間會遠大於6字節,在我的機器上,通過malloc_usable_size發現申請6字節,返回的chunk,實際可用的size為24,加上首尾部就更多了。

但你真正申請(可用)的大小是6字節,可見,動態內存分配的chunk內有大量的碎片,這就是內碎片,而外碎片是存在chunk之間的,是另一個問題。

當你申請的size較大,有效載荷 / 耗費空間的比例會比較高,內碎片佔比不高,但但size較小,這個佔比就高,如果這種小size的chunk非常多,就會造成內存的極大浪費。

《C++設計新思維》一書中的loki庫實現了一個小對象分配器,通過隱式鏈表的方式解決了這個問題,有興趣的可以去看看。

(b)cached obj

《C++ Primer》實現了一個CachedObj類模板,任何需要擁有這種cached能力的類型都可以通過從CachedObj<T>派生而獲得。

它的主要思想是為該種類型維護一個FreeList,每個節點就是一個Object,對象申請的時候,檢查FreeList,如果FreeList不為空,則摘除頭結點返回,如果FreeList為空,則new一批Object並串到FreeList,然後走FreeList不為空的分配流程,通過重載類的operator new和operator delete,達到對類的使用者透明的目的。

(c)內存分配和對象構建分離

c的malloc用來動態分配內存,free用來歸還內存;C++的new做了3件事,通過operator new(本質上等同malloc)分配內存,在分配的內存上構建對象,返回對象指針;而delete幹了兩件事,調用析構函數,歸還內存。

C++通過placement new可以分離內存分配和對象構建,結合顯示的析構函數調用,達到自控的目的。

我優化過一個遊戲項目,啟動時間過長,記憶中需要幾十秒(至少十幾秒),分析後發現主要是因為遊戲執行預分配策略(對象池),在啟動的時候按最大容量創建怪和玩家,對象構建很重,大量對象構建耗時過長,通過分離內存分配和對象構建,把對象構建推遲到真正需要的時候,實現了服務的重啟秒起。

(d)內存復用

編解碼、加解密、序列化反序列化(marshal/unmarshal)的時候一般都需要動態申請內存,這種調用頻次很高,可以考慮用靜態內存,為了避免多線程競爭,可以用thread local。

當然你也可以改進靜態內存策略,比如封裝一個GetEncodeMemeory(size_t)函數,維護一個void* + size_t結構體對象(初始化為NULL+0),對比參數size跟對象的size成員,如果參數size<=對象size,直接返回對象大的void*指針,否則free掉void*指針,再按參數size分配一個更大的void*,並用參數size更新對象size。

### cache優化

i-cache優化:i-cache的優化可以通過精簡code path,簡化調用關係,減少代碼量,減少複雜度來實現。

具體措施包括,減少函數調用(就地展開、inline),利用分支預測,減少函數指針,可以考慮把code path上相關的函數定義在一起,把相關的函數定義到一個源文件,並讓它們在源文件上臨近,這樣生成的object文件,在運行時加載後相關函數大概率也內存臨近,當然編譯器也一直在做這方面的努力,但我們寫代碼不應該依賴編譯器優化,盡量去幫助編譯器生成更高效的代碼。

d-cache優化:d-cache優化包括改進數據結構和算法獲取更好的數據訪問時空局部性,比如二分查找就是d-cache友好算法。一個cache line一般是64B,如果數據跨越兩個cache-line,則會導致load & store2次,所以,需要結合cache對齊,盡量讓相關訪問的數據在一個cache-line。

如果結構體過大,則各成員不僅可能在不同cache-line,甚至可能在不同page,所以應該避免結構體過大。

如果結構體的成員變量過多,一般而言對各成員的訪問頻次也會滿足2-8定律,可以考慮把hot和cold的成員分開,重排結構體成員變量順序,但這些騷操作我不建議在開始的時候用,因為說不定哪天又要增刪成員,從而破壞苦心孤詣搭建的積木。

### 判斷前置

判斷前置指在函數中講判斷返回的語句前置,這樣不至於忙活半天,你跟我說對不起不合適,要杜絕這種騙pao的做法。

在寫多個判斷的時候,把不滿足可能性高的放在前面。

在寫條件或的時候把為true的放在前面,在寫條件與的時候把為false的放在前面。

另外,如果在循環里調用一個函數,而這個函數里檢查某條件,不符合就返回,這種情況,可以考慮把檢查放到調用函數的外面,這樣不滿足的話就不用陷入函數,當然,你也可以說,這樣的操作違背軟件工程,但看你想要什麼,你不總是能夠兩全其美,對吧?

### 湊零為整與化整為零

湊零為整其實的思想在日誌批處理里提了,不再展開。

化整為零體現了分而治之的思想,可以把一個大的操作,分攤開來,避免在做大操作的時候導致卡頓,從而讓CPU佔比更加平穩。

### 分頻

之前我優化過一個遊戲服務器,遊戲服務器的邏輯線程是一個大循環,裡面調用tick函數,tick函數里調用了所有需要check timer & do的事情,然後所有需要check timer & do的事情都塞進tick里。

改進:tick里調用了tick50ms、tick100ms、tick500ms,tick1000ms,tick5000ms,然後把需要check timer & do的邏輯根據精度要求塞到不同的tickXXms里去。

### 減法

減少冗餘

減少拷貝、零拷貝

減少參數個數(寄存器參數、取決於ABI約定)

減少函數調用次數/層次

減少存儲引用次數

減少無效初始化和重複賦值

### 循環優化

這方面的知識很多,感覺一下子講不完,提幾點,循環套循環要內大外小,盡量把邏輯提取到循環外。

提取與循環無關的表達式,盡量減少循環內不必要計算。

循環內盡量使用局部變量。

循環展開是一種程序變換,通過增加每次迭代計算的元素的數量,減少循環的迭代次數。

還有循環分塊的騷操作。

### 防禦性編程適可而止

有兩個流派,一個是完全的不信任,即所有函數調用里都對參數判斷,包括判空,有效性檢查等,但這樣做有幾點不好:

第一,它只是貌似更安全,並不是真的更安全。

第二,它稀釋代碼濃度,淹沒關鍵語句。

第三,如果通過返回值報告錯誤,則加重了調用者負擔,調用者需要添加額外代碼檢查,不然更奇怪。

第四,重複判斷空耗CPU。

第五,埋雷,把本該crash或者暴露的問題埋得更深。

但這種做法大行其道,它有一定的市場和道理。

另一個是界定邊界,區分公開接口和內部實現,檢查只在模塊之間進行,就相當於進園區的時候,門衛會檢查你證件,但之後,則不再檢查。因為內部實現是受控的安全上下文,開發者應該完全cover住。

我主張防禦性編程適可而止,但現實中,軟件開發通常多人合作,每個開發者素質不一樣,這就是客觀現實,所以我也理解前一種做法。

### release乾淨

開發過程中,我們會加很多診斷信息,比如我們可能接管內存分配,從而附加額外的首尾部,通過填寫magic Num捕獲異常或者內存越界,但這些信息應該只用於開發階段的DEBUG需要,在release階段應該通過預處理的方式刪除掉。

日誌分級其實也體現了這種思想,通常有兩種做法,一個是定義級別變量,另一個是預處理,預處理乾淨,但需要重新編譯生成image,而變量更靈活,但變量的比較還是有開銷的。

不要忽視這些診斷調試信息的開銷,牢記不必做的事情絕不做的原則。

### 慎用遞歸

遞歸的寫法簡單,理解起來也容易,但遞歸是函數調用,有棧幀建立撤銷控制跳轉的開銷,另外也有爆棧的風險,在性能敏感關鍵路徑,優先考慮用非遞歸版本。

## 4、編譯優化(寫不動了)

### inline

### restrict

### LTO

### PGO

### 優化選項

## 5、其他優化(不想寫了)

### 綁核

### SIMD

### 鎖與並發

#### 鎖的粒度

#### 無鎖編程

#### Per-cpu data structure & thread local

#### 內存屏障

#### 異構優化/TCO優化

比如用GPGPU、FPGA、SmartNIC來offload原來cpu的任務,TCO優化指的是不以性能優化為單一指標,而是在滿足性能條件下以綜合成本為優化直播,當然異構也包括主動利用CPU的avx或者其他邏輯單元,這類優化往往編譯器不能自動展開(@zrg)

常識和數據

CPU拷貝數據一般一秒鐘能做到幾百兆,當然每次拷貝的數據長度不同,吞吐不同。

一次函數執行如果耗費超過1000 cycles就比較大了(刨除調用子函數的開銷)。

pthread_mutex_t是futex實現,不用每次都進入內核,首次加解鎖大概耗時4000-5000 cycles左右,之後,每次加解鎖大概120 cycles,O2優化的時候100 cycles,spinlock耗時略少。

lock內存總線+xchg需要50 cycles,一次內存屏障要50 cycles。

有一些無鎖的技術,比如CAS,比如linux kernel里的kfifo,主要利用了整型迴繞+內存屏障。

幾個如何?

1. 如何定位CPU瓶頸?

CPU是通常大家最先關注的性能指標,宏觀維度有核的CPU使用率,微觀有函數的CPU cycle數,根據性能的模型,性能規格與CPU使用率是互相關聯的,規格越高,CPU使用率越高,但是處理器的性能往往又受到內存帶寬、Cache、發熱等因素的影響,所以CPU使用率和規格參數之間並不是簡單的線性關係,所以性能規格翻倍並不能簡單地翻譯成我們的CPU使用率要優化一倍。

至於CPU瓶頸的定位工具,最有名也是最有用的工具就是perf,它是性能分析的第一步,可以幫我們找到系統的熱點函數。就像人看病一樣,只知道癥狀是不夠的,需要通過醫療機器進一步分析病因,才能對症下藥。

所以我們通過性能分析工具PMU或者其他工具去進一步分析CPU熱點的原因比如是指令數本身就比較多,還是Cache miss導致的等,這樣在做性能優化的時候不會走偏。

優化CPU的目標就是讓CPU運行不受阻礙。

2. 如何定位IO瓶頸?

系統IO的瓶頸可以通過CPU和負載的非線性關係體現出來。當負載增大時,系統吞吐量不能有效增大,CPU不能線性增長,其中一種可能是IO出現阻塞。

系統的隊列長度特別是發送、寫磁盤線程的隊列長度也是IO瓶頸的一個間接指標。

對於網絡系統來講,我建議先從外部觀察系統。所謂外部觀察是指通過觀察外部的網絡報文交換,可以用tcpdump, wireshark等工具,抓包看一下。

比如我們優化一個RPC項目,它的吞吐量是10TPS,客戶希望是100TPS。我們使用wireshark抓取TCP報文流,可以分析報文之間的時間戳,響應延遲等指標來判斷是否是由網絡引起來的。

然後可以通過netstat -i/-s選項查看網絡錯誤、重傳等統計信息。還可以通過iostat查看cpu等待IO的比例。IO的概念也可以擴展到進程間通信。

對於磁盤類的應用程序,我們最希望看到寫磁盤有沒有時延、頻率如何。其中一個方法就是通過內核ftrace、perf-event事件來動態觀測系統。比如記錄寫塊設備的起始和返回時間,這樣我們就可以知道磁盤寫是否有延時,也可以統計寫磁盤時間耗費分布。有一個開源的工具包perf-tools裡面包含着iolatency, iosnoop等工具。

3. 如何定位IO瓶頸?

應用程序常用的IO有兩種:Disk IO和網絡IO。判斷系統是否存在IO瓶頸可以通過觀測系統或進程的CPU的IO等待比例來進行,比如使用mpstat、top命令。

系統的隊列長度特別是發送、寫磁盤線程的隊列長度也是IO瓶頸的一個重要指標。

對於網絡 IO來講,我們可以先使用netstat -i/-s查看網絡錯誤、重傳等統計信息,然後使用sar -n DEV 1和sar -n TCP,ETCP 1查看網路實時的統計信息。ss (Socket Statistics)工具可以提供每個socket相關的隊列、緩存等詳細信息。

更直接的方法可以用tcpdump, wireshark等工具,抓包看一下。

對於Disk IO,我們可以通過iostat -x -p xxx來查看具體設備使用率和讀寫平均等待時間。如果使用率接近100%,或者等待時間過長,都說明Disk IO出現飽和。

一個更細緻的觀察方法就是通過內核ftrace、perf-event來動態觀測Linux內核。比如記錄寫塊設備的起始和返回時間,這樣我們就可以知道磁盤寫是否有延時,也可以統計寫磁盤時間耗費分布。有一個開源的工具包perf-tools裡面包含着iolatency, iosnoop等工具。

4.如何定位鎖的問題?

大家都知道鎖會引入額外開銷,但鎖的開銷到底有多大,估計很多人沒有實測過,我可以給一個數據,一般單次加解鎖100 cycles,spinlock或者cas更快一點。

使用鎖的時候,要注意鎖的粒度,但鎖的粒度也不是越小越好,太大會增加撞鎖的概率,太小會導致代碼更難寫。

多線程場景下,如果cpu利用率上不去,而系統吞吐也上不去,那就有可能是鎖導致的性能下降,這個時候,可以觀察程序的sys cpu和usr cpu,這個時候通過perf如果發現lock的開銷大,那就沒錯了。

如果程序卡住了,可以用pstack把堆棧打出來,定位死鎖的問題。

5. 如何提⾼Cache利用率?

內存/Cache問題是我們常見的負載瓶頸問題,通常可利用perf等一些通用工具來輔助分析,優化cache的思想可以從兩方面來着手,一個是增加局部數據/代碼的連續性,提升cacheline的利用率,減少cache miss,另一個是通過prefetch,降低miss帶來的開銷。

通過對數據/代碼根據冷熱進行重排分區,可提升cacheline的有效利用率,當然觸發false-sharing另當別論,這個需要根據運行trace進行深入調整了;說到prefetch,用過的人往往都有一種體會,現實效果比預期差的比較遠,確實無論是數據prefetch還是代碼prefetch,不確定性太大,指望編譯器更靠譜點。

小結

性能優化是一項細緻的工作,工程師們曾致力於尋找一勞永逸解決性能問題的捷徑,但遺憾的是,沒有銀彈,但這並不意味着性能優化無章可循。軟件工程師們在性能優化方面積累了大量的經驗,包括架構、緩存、預取、工具、編譯器與編程語言,代碼重構等實踐經驗方方面面,這些方法和探討都具有借鑒意義。
性能優化也是一個系統性工程,出現性能瓶頸再優化是一種先污染後治理的思路。更好的方式是將性能貫穿於軟件的整個生命周期之中,在設計之初即把性能作為一項需求甚至關鍵目標加以考慮,開發中持續監控性能的變化並嚴格遵從高性能編碼規範,後期維護將性能納入維護體系。

嚴格的說,性能優化和性能設計有所不同,性能優化通常是在現有系統和代碼基礎上做改進,它並非推倒重來,考驗的是開發者反向修復的能力,而性能設計考驗的是設計者的正向設計能力,但性能優化的方法可以指導性能設計,兩者互補。

原創文章,作者:投稿專員,如若轉載,請註明出處:https://www.506064.com/zh-hant/n/256098.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
投稿專員的頭像投稿專員
上一篇 2024-12-15 12:33
下一篇 2024-12-15 12:33

相關推薦

發表回復

登錄後才能評論