性能優化指南:性能優化的通常性原則與方法

  做爲一個程序員,性能優化是常有的事情,無論是桌面應用仍是web應用,無論是前端仍是後端,無論是單點應用仍是分佈式系統。本文從如下幾個方面來思考這個問題:性能優化的通常性原則,性能優化的層次,性能優化的通用方法。本文不限於任何語言、框架,不過可能會用Python語言來舉例。html

  不過囿於我的經驗,可能更多的是從Linux服務端的角度來思考這些問題。前端

  本文地址:http://www.cnblogs.com/xybaby/p/9055734.htmlpython

通常性原則

依據數據而不是憑空猜想

  這是性能優化的第一原則,當咱們懷疑性能有問題的時候,應該經過測試、日誌、profillig來分析出哪裏有問題,有的放矢,而不是憑感受、撞運氣。一個系統有了性能問題,瓶頸有多是CPU,有多是內存,有多是IO(磁盤IO,網絡IO),大方向的定位可使用top以及stat系列來定位(vmstat,iostat,netstat...),針對單個進程,可使用pidstat來分析。mysql

  在本文中,主要討論的是CPU相關的性能問題。按照80/20定律,絕大多數的時間都耗費在少許的代碼片斷裏面,找出這些代碼惟一可靠的辦法就是profile,我所知的編程語言,都有相關的profile工具,熟練使用這些profile工具是性能優化的第一步。ios

忌過早優化

The real problem is that programmers have spent far too much time worrying about efficiency in the wrong places and at the wrong times; premature optimization is the root of all evil (or at least most of it) in programming.nginx

  我並不十分清楚Donald Knuth說出這句名言的上下文環境,但我本身是十分認同這個觀念的。在個人工做環境(以及典型的互聯網應用開發)與編程模式下,追求的是快速的迭代與試錯,過早的優化每每是無用功。並且,過早的優化很容易拍腦殼,優化的點每每不是真正的性能瓶頸。程序員

忌過分優化

As performance is part of the specification of a program – a program that is unusably slow is not fit for purposeweb

  性能優化的目標是追求合適的性價比。redis

  在不一樣的階段,咱們對系統的性能會有必定的要求,好比吞吐量要達到多少多少。若是達不到這個指標,就須要去優化。若是能知足預期,那麼就無需花費時間精力去優化,好比只有幾十我的使用的內部系統,就不用按照十萬在線的目標去優化。算法

  並且,後面也會提到,一些優化方法是「有損」的,可能會對代碼的可讀性、可維護性有反作用。這個時候,就更不能過分優化。

深刻理解業務

  代碼是服務於業務的,也許是服務於最終用戶,也許是服務於其餘程序員。不瞭解業務,很難理解系統的流程,很難找出系統設計的不足之處。後面還會說起對業務理解的重要性。

性能優化是持久戰

  當核心業務方向明確以後,就應該開始關注性能問題,當項目上線以後,更應該持續的進行性能檢測與優化。

  如今的互聯網產品,再也不是一錘子買賣,在上線以後還須要持續的開發,用戶的涌入也會帶來性能問題。所以須要自動化的檢測性能問題,保持穩定的測試環境,持續的發現並解決性能問題,而不是被動地等到用戶的投訴。

選擇合適的衡量指標、測試用例、測試環境

  正由於性能優化是一個長期的行爲,因此須要固定衡量指標、測試用例、測試環境,這樣才能客觀反映性能的實際狀況,也能展示出優化的效果。

  衡量性能有不少指標,好比系統響應時間、系統吞吐量、系統併發量。不一樣的系統核心指標是不同的,首先要明確本系統的核心性能訴求,固定測試用例;其次也要兼顧其餘指標,不能顧此失彼。

  測試環境也很重要,有一次忽然發現咱們的QPS高了許多,可是程序壓根兒沒優化,查了半天,才發現是換了一個更牛逼的物理機作測試服務器。

性能優化的層次

  按照個人理解能夠分爲需求階段,設計階段,實現階段;越上層的階段優化效果越明顯,同時也更須要對業務、需求的深刻理解。

需求階段

不戰而屈人之兵,善之善者也

  程序員的需求可能來自PM、UI的業務需求(或者說是功能性需求),也可能來自Team Leader的需求。當咱們拿到一個需求的時候,首先須要的是思考、討論需求的合理性,而不是馬上去設計、去編碼。

  需求是爲了解決某個問題,問題是本質,需求是解決問題的手段。那麼需求是否可否真正的解決問題,程序員也得本身去思考,在以前的文章也提到過,產品經理(特別是知道一點技術的產品經理)的某個需求可能只是某個問題的解決方案,他認爲這個方法能夠解決他的問題,因而把解決方案當成了需求,而不是真正的問題。

  需求討論的前提對業務的深刻了解,若是不瞭解業務,根本無法討論。即便需求已經實現了,當咱們發現有性能問題的時候,首先也能夠從需求出發。

  需求分析對性能優化有什麼幫助呢,第一,爲了達到一樣的目的,解決一樣問題,也許能夠有性能更優(消耗更小)的辦法。這種優化是無損的,即不改變需求本質的同時,又能達到性能優化的效果;第二種狀況,有損的優化,即在不明顯影響用戶的體驗,稍微修改需求、放寬條件,就能大大解決性能問題。PM退步一小步,程序前進一大步。

  需求討論也有助於設計時更具擴展性,應對將來的需求變化,這裏按下不表。

設計階段

  高手都是花80%時間思考,20%時間實現;新手寫起代碼來很快,但後面是無窮無盡的修bug

  設計的概念很寬泛,包括架構設計、技術選型、接口設計等等。架構設計約束了系統的擴展、技術選型決定了代碼實現。編程語言、框架都是工具,不一樣的系統、業務須要選擇適當的工具集。若是設計的時候作的不夠好,那麼後面就很難優化,甚至須要推到重來。

實現階段

  實現是把功能翻譯成代碼的過程,這個層面的優化,主要是針對一個調用流程,一個函數,一段代碼的優化。各類profile工具也主要是在這個階段生效。除了靜態的代碼的優化,還有編譯時優化,運行時優化。後兩者要求就很高了,程序員可控性較弱。

   代碼層面,形成性能瓶頸的緣由一般是高頻調用的函數、或者單次消耗很是高的函數、或者兩者的結合。

  

  下面介紹針對設計階段與實現階段的優化手段。

通常性方法

緩存

  沒有什麼性能問題是緩存解決不了的,若是有,那就再加一級緩存

a cache /kæʃ/ KASH,[1] is a hardware or software component that stores data so future requests for that data can be served faster; the data stored in a cache might be the result of an earlier computation, or the duplicate of data stored elsewhere.

  緩存的本質是加速訪問,訪問的數據要麼是其餘數據的副本 -- 讓數據離用戶更近;要麼是以前的計算結果 -- 避免重複計算.

  緩存須要用空間換時間,在緩存空間有限的狀況下,須要優秀的置換換算來保證緩存有較高的命中率。

數據的緩存

  這是咱們最多見的緩存形式,將數據緩存在離使用者更近的地方。好比操做系統中的CPU cache、disk cache。對於一個web應用,前端會有瀏覽器緩存,有CDN,有反向代理提供的靜態內容緩存;後端則有本地緩存、分佈式緩存。

  數據的緩存,不少時候是設計層面的考慮。

  對於數據緩存,須要考慮的是緩存一致性問題。對於分佈式系統中有強一致性要求的場景,可行的解決辦法有lease,版本號。

計算結果的緩存

  對於消耗較大的計算,能夠將計算結果緩存起來,下次直接使用。

  咱們知道,對遞歸代碼的一個有效優化手段就是緩存中間結果,lookup table,避免了重複計算。python中的method cache就是這種思想.

  對於可能重複建立、銷燬,且建立銷燬代價很大的對象,好比進程、線程,也能夠緩存,對應的緩存形式如單例、資源池(鏈接池、線程池)。

  對於計算結果的緩存,也須要考慮緩存失效的狀況,對於pure function,固定的輸入有固定的輸出,緩存是不會失效的。但若是計算受到中間狀態、環境變量的影響,那麼緩存的結果就可能失效,好比我在前面提到的python method cache

併發

  一我的幹不完的活,那就找兩我的幹。併發既增長了系統的吞吐,又減小了用戶的平均等待時間。

  這裏的併發是指廣義的併發,粒度包括多機器(集羣)、多進程、多線程。

  對於無狀態(狀態是指須要維護的上下文環境,用戶請求依賴於這些上下文環境)的服務,採用集羣就能很好的伸縮,增長系統的吞吐,好比掛載nginx以後的web server

  對於有狀態的服務,也有兩種形式,每一個節點提供一樣的數據,如mysql的讀寫分離;每一個節點只提供部分數據,如mongodb中的sharding

  分佈式存儲系統中,partition(sharding)和replication(backup)都有助於併發。

  絕大多數web server,要麼使用多進程,要麼使用多線程來處理用戶的請求,以充分利用多核CPU,再有IO阻塞的地方,也是適合使用多線程的。比較新的協程(Python greenle、goroutine)也是一種併發。

惰性

  將計算推遲到必需的時刻,這樣極可能避免了多餘的計算,甚至根本不用計算,這個在以前的《lazy ideas in programming》一文中舉了許多例子。

  CopyOnWrite這個思想真牛逼!

批量,合併

  在有IO(網絡IO,磁盤IO)的時候,合併操做、批量操做每每能提高吞吐,提升性能。

  咱們最多見的是批量讀:每次讀取數據的時候多讀取一些,以備不時之需。如GFS client會從GFS master多讀取一些chunk信息;如分佈式系統中,若是集中式節點複雜全局ID生成,俺麼應用就能夠一次請求一批id。

  特別是系統中有單點存在的時候,緩存和批量本質上來講減小了與單點的交互,是減輕單點壓力的經濟有效的方法

  在前端開發中,常常會有資源的壓縮和合並,也是這種思想。

  當涉及到網絡請求的時候,網絡傳輸的時間可能遠大於請求的處理時間,所以合併網絡請求就頗有必要,好比mognodb的bulk operation,redis 的pipeline。寫文件的時候也能夠批量寫,以減小IO開銷,GFS中就是這麼幹的

更高效的實現

  同一個算法,確定會有不一樣的實現,那麼就會有不一樣的性能;有的實現多是時間換空間,有的實現多是空間換時間,那麼就須要根據本身的實際狀況權衡。

  程序員都喜歡早輪子,用於練手無可厚非,但在項目中,使用成熟的、通過驗證的輪子每每比本身造的輪子性能更好。固然無論使用別人的輪子,仍是本身的工具,當出現性能的問題的時候,要麼優化它,要麼替換掉他。

  好比,咱們有一個場景,有大量複雜的嵌套對象的序列化、反序列化,開始的時候是使用python(Cpython)自帶的json模塊,即便發現有性能問題也無法優化,網上一查,替換成了ujson,性能好了很多。

  上面這個例子是無損的,但一些更高效的實現也多是有損的,好比對於python,若是發現性能有問題,那麼極可能會考慮C擴展,但也會帶來維護性與靈活性的喪失,面臨crash的風險。

縮小解空間

  縮小解空間的意思是說,在一個更小的數據範圍內進行計算,而不是遍歷所有數據。最多見的就是索引,經過索引,可以很快定位數據,對數據庫的優化絕大多數時候都是對索引的優化。

  若是有本地緩存,那麼使用索引也會大大加快訪問速度。不過,索引比較適合讀多寫少的狀況,畢竟索引的構建也是需有消耗的。

  另外在遊戲服務端,使用的分線和AOI(格子算法)也都是縮小解空間的方法。

性能優化與代碼質量

  不少時候,好的代碼也是高效的代碼,各類語言都會有一本相似的書《effective xx》。好比對於python,pythonic的代碼一般效率都不錯,如使用迭代器而不是列表(python2.7 dict的iteritems(), 而不是items())。

  衡量代碼質量的標準是可讀性、可維護性、可擴展性,但性能優化有可能會違背這些特性,好比爲了屏蔽實現細節與使用方式,咱們會可能會加入接口層(虛擬層),這樣可讀性、可維護性、可擴展性會好不少,可是額外增長了一層函數調用,若是這個地方調用頻繁,那麼也是一筆開銷;又如前面提到的C擴展,也是會下降可維護性、

  這種有損代碼質量的優化,應該放到最後,不得已而爲之,同時寫清楚註釋與文檔。

  爲了追求可擴展性,咱們常常會引入一些設計模式,如狀態模式、策略模式、模板方法、裝飾器模式等,但這些模式不必定是性能友好的。因此,爲了性能,咱們可能寫出一些反模式的、定製化的、不那麼優雅的代碼,這些代碼實際上是脆弱的,需求的一點點變更,對代碼邏輯可能有相當重要的影響,因此仍是回到前面所說,不要過早優化,不要過分優化。

總結

  來張腦圖總結一下

  

相關文章
相關標籤/搜索