同一份邏輯,不同人的實現的程式碼效能會出現數量級的差異; 同一份程式碼,你可能微調幾個字元或者某行程式碼的順序,就會有數倍的效能提升;同一份程式碼,也可能在不同處理器上執行也會有幾倍的效能差異;十倍程式設計師不是隻存在於傳說中,可能在我們的周圍也比比皆是。十倍體現在程式設計師的方法面面,而程式碼效能卻是其中最直觀的一面。
本文是《如何寫出高效能程式碼》系列的第四篇,本文將告訴你資料訪問會怎麼樣影響到程式的效能,以及如何通過變更資料訪問的方式提升程式的效能。
資料訪問速度為什麼會影響到程式的效能?
程式的執行的每一個可以簡化為這樣一個三步模型:第一步,讀資料(當然也有部分資料是別的地方法發過來的);第二步,對資料做處理;第三步,將處理完的結果寫入儲存器。這裡我將這三步驟簡稱為 讀算寫。 實際上真實的CPU指令執行過程會稍微複雜有些,但實際上也是這三個步驟。 而一個複雜的程式包含無數個CPU指令,如果讀取或者寫入資料太慢,必然會影響到程式的效能。
為了能更直觀一點,我這裡將程式執行的流程比作是大廚做菜,大廚的工作流程就是取原始食材,然後對食材進行加工(煎烤烹炸煮),最後出鍋上菜。影響大廚出菜速度的因素除了加工過程之前,獲取食材的耗時也會影響到大廚出菜速度。有些食材就在手邊,可以很快獲取到,但有些食材可能在冷庫、甚至在菜市場,獲取就很不方便了。
CPU猶如大廚,而資料就是CPU的食材,暫存器裡的資料就是CPU手邊的食材,記憶體的資料就是在冷庫的食材,固態硬碟(SSD)上的資料是還在菜市場的食材,機械硬碟(HDD)上的資料猶如還在地裡生長的菜…… 如果CPU在執行程式時,如果拿不到所需要的資料,它也只能等在那兒浪費時間了。
資料訪問速度對程式效能有多大影響?
不同儲存器資料讀取和寫入的時延相差極大,鑑於大多數場景下,我們都是讀取資料,我們就只拿資料讀取為例,最快的暫存器和最慢的機械磁碟,隨機讀寫的時延相差百萬倍。可能你沒有直觀概念,我們還是拿廚師做個類比。
假設廚師要做一道蕃茄炒雞蛋,如果食材都有人備好的話,只需要十來秒食材就能下鍋炒制。 我們把這個時間比作是CPU從暫存器裡取到資料的時間。然而如果是CPU從磁碟獲取資料的話,所耗費的時間相當於廚師自己種出蕃茄或者養小雞下蛋了(3-4個月)。由此可見,從錯誤的儲存裝置上獲取資料,會極大影響程式的執行速度。
再說一個我們之前在生產環境遇到的實際案例,我們在生產環境也出過故障。原因是這樣的,我們有個服務容器化改造的時候,和上游服務沒有部署在同一個機房,跨機房雖然只會增加1ms的時延,但他們服務程式碼寫的有問題,有個介面批量序列調另外一個服務,序列累加導致介面時延增加上百ms。 本來沒有效能問題的服務,就因為遷移了機房,導致效能出現了問題……
各儲存器效能差異
實際上在編碼的時候,遇到的儲存裝置多種多樣,暫存器、記憶體、磁碟、網路儲存……,每種裝置都有自己的特點。只有認識到各種儲存器之間的差異,我們才能在正確的場景下使用合適的儲存器。以下表格就是各類常見儲存裝置的隨機讀時延參考資料……
備註:以上資料在不同硬體裝置會有出入,這裡只是為了展示其差異性,不代表準確值,準確資訊請參考硬體手冊。
雖然日常我們覺得記憶體的讀取速度已經很非常快了,日常寫程式碼的時候遇到啥資料獲取比較慢,加個記憶體快取速度簡直就起飛了。但記憶體的訪問速度相對於CPU執行速度來說還是太慢,讀取一次記憶體的時間,都夠CPU執行幾百條指令了,所以現代CPU都對記憶體加了快取。
如何減小資料訪問時延對效能的影響?
減少資料訪問時延對效能的影響也很簡單,那就是把資料儘可能放到最快的儲存介質上。然而,存取速度、容量、價格三者之間有著不可調和的矛盾,簡單來說就是 速度越快容量越小但價格越貴,反之容量越大速度越慢而價格越便宜。
世界總是那麼巧秒,彷彿一切早被安排好,我們並不需要把所有的資料都放在最快的儲存介質上。 還記得我們在第二篇(巧用資料特性)[https://blog.csdn.net/xindoo/article/details/123941141] 提到的資料區域性性嗎! 區域性性分兩種,空間區域性性和時間區域性性。
- 時間區域性性: 如果一份某個時刻資料被訪問過,那不久之後這份資料會被再次訪問到。
- 空間區域性性: 如果某個儲存元被訪問過,大概率那不久之後,其附近的儲存單元也會被訪問。
總結下這兩點就是,程式大部分時間只會集中訪問很小的一部分資料。 這意味著我們可以用較小的儲存空間覆蓋到大部分被訪問的資料。 說直接點就是,我們可以加快取。 實際上,不管是計算機硬體、資料庫、還是業務系統,到處都充斥著快取。甚至你寫下的每一行程式碼,在機器上執行時都用到了快取,不知道大家有沒有關注過CPU,CPU有個引數,就是快取大小,我們以intel酷睿i7-12650HX 為例,它就有24MB的三級快取,這個快取就是CPU到記憶體之間的快取。 只不過現代計算機將底層的細節遮蔽掉了而已,我們日常不太可能主要的到。
在我們自己寫程式碼的時候,也可以加快取來提升程式效能。舉個最近的我們在系統中遇到的例子,我們最新在做資料許可權相關的功能,不同的員工在我們系統中有不同的許可權,所以他們看到的資料也應該是不同的。我們的實現方式是每個使用者請求系統的時候,首先獲取到該使用者所有的許可權列表,然後把所有在許可權列表中的資料展示出來。
因為每個人的許可權列表比較大,所以許可權介面的效能不怎麼樣,每次請求耗時也比較長。所以,我們直接給這個介面的資料加了快取,優先從快取裡取,取不到再調介面,極大提升了程式效能。當然因為許可權資料也不會經常變動,所以也不用太考慮資料滯後導致的後果。另外,我們快取資料只加了幾分鐘,因為一個使用者單次使用我們系統時長也就持續幾分鐘,過幾分鐘後資料過期快取空間也會自動釋放,達到節省空間的目的。
我在上大學那會,膝上型電腦還是標配機械硬碟的年代,那時候電腦永久了會很卡,後來瞭解到換裝SSD會提升電腦效能,那個時候SSD還挺貴的,普通筆本都不會標配SSD後來我攢半個月的生活費給自己筆記本替換了一塊120g的SSD,電腦的執行速度就有明顯的提升,本質上還是因為SSD的隨機訪問時延比機械硬碟快上百倍的原因。 之前某大廠號稱將mysql效能提升了上百倍,其實也是基於SSD做的很多查詢優化。
快取不是銀彈
銀彈(英文:Silver Bullet),指由純銀質或鍍銀的子彈。在歐洲民間傳說及19世紀以來哥特小說風潮的影響下,銀色子彈往往被描繪成具有驅魔功效的武器,是針對狼人、吸血鬼等超自然怪物的特效武器。後來也被比喻為具有極端有效性的解決方法,作為殺手鐗、最強殺招、王牌等的代稱。
這裡特別提醒下,快取不是萬能的,快取其實是有副作用的,那就是資料的有效性很難得到保證。快取其實裡面放的是舊資料,當前時刻資料是不是還是這樣的?不確定,也許資料早就變了,所以使用快取時必須要關注快取資料有效性問題。如果快取時間過久,資料失效的可能效能,資料不一致導致的風險也就越大。 如果快取時間過短,因為經常需要獲取原始資料,快取存在意義也就越小。所以在使用快取必須要做出資料不一致和效能之間的權衡(trade-off),你需要正確評估資料的時效性,對快取設定合理的過期策略。
上文說到其實我們寫下的每一行程式碼都用到了快取,現在大家已經都知道這個快取其實就是CPU的Cache。CPU的Cache也是有明顯的副作用的,我們在寫多執行緒程式碼的時候也不得不關注到,那就是多核CPU之間資料一致性的問題。因為CPU Cache的存在,我們寫多執行緒程式碼時不得不考慮資料同步的問題,導致多執行緒的程式碼很難編寫,出了問題也很難排查。
有個面試八股文題目其實就很容易說明這個問題——多執行緒計數器,多執行緒去操作計數器,累加統計資料,如何保證資料統計的準確性。如果只是簡單使用cnt++實現,這裡就會遇到多核CPU快取導致的資料不一致性,具體原理這裡不再解釋,反正結果就是統計出來的資料會比真是資料少。 正確的做法就是,你必須在累加的過程中加多執行緒同步的機制,保證同一時刻只可能有一個執行緒在操作,操作完之後也能保證資料能寫回記憶體,在java中必須使用鎖或者原子類實現。而這對於程式設計新手而言又是一道門檻。
總結
資料訪問是任何程式不可或缺的一部分,甚至對大多數程式而言時間都耗費在了資料訪問的過程上,所以只要優化了這部分的耗時,程式的效能必然能得到提升。
本文全部內容就到這了,下一篇,我們將繼續探討下效能優化到極致該怎麼做,敬請期待!!另外,有興趣也可以查閱下之前的幾篇文章。