區域性性原理——各類優化的基石

xindoo發表於2019-08-05

學過計算機底層原理、瞭解過很多架構設計或者是做過優化的同學,應該很熟悉區域性性原理。即便是非計算機行業的人,在做各種調優、提效時也不得不考慮到區域性性,只不過他們不常用區域性性一詞。如果抽象程度再高一些,甚至可以說地球、生命、萬事萬物都是區域性性的產物,因為這些都是宇宙中熵分佈佈局、區域性的熵低導致的,如果宇宙中處處熵一致,有的只有一篇混沌。  
  所以什麼是 區域性性 ?這是一個常用的計算機術語,是指處理器在訪問某些資料時短時間記憶體在重複訪問,某些資料或者位置訪問的概率極大,大多數時間只訪問_區域性_的資料。基於區域性性原理,計算機處理器在設計時做了各種優化,比如現代CPU的多級Cache、分支預測…… 有良好區域性性的程式比區域性性差的程式執行得更快。雖然區域性性一詞源於計算機設計,但在當今分散式系統、網際網路技術裡也不乏區域性性,比如像用redis這種memcache來減輕後端的壓力,CDN做素材分發減少頻寬佔用率……
  區域性性的本質是什麼?其實就是概率的不均等,這個宇宙中,很多東西都不是平均分佈的,平均分佈是概率論中幾何分佈的一種特殊形式,非常簡單,但世界就是沒這麼簡單。我們更長聽到的釋出叫做高斯釋出,同時也被稱為正態分佈,因為它就是正常狀態下的概率釋出,起概率圖如下,但這個也不是今天要說的。
在這裡插入圖片描述
  其實有很多情況,很多事物有很強的頭部集中現象,可以用概率論中的泊松分佈來刻畫,這就是區域性性在概率學中的刻畫形式。
在這裡插入圖片描述
在這裡插入圖片描述
  上面分別是泊松分佈的示意圖和概率計算公式,$\lambda$ 表示單位時間(或單位面積)內隨機事件的平均發生次數,$e$表示自然常數2.71828..,k表示事件發生的次數。要注意在刻畫區域性性時$\lambda$表示不命中高頻資料的頻度,$\lambda$越小,頭部集中現象越明顯。

區域性性分類

  區域性性有兩種基本的分類, 時間區域性性空間區域性性 ,按Wikipedia的資料,可以分為以下五類,其實有些就是時間區域性性和空間區域性性的特殊情況。

時間區域性性(Temporal locality):

  如果某個資訊這次被訪問,那它有可能在不久的未來被多次訪問。時間區域性性是空間區域性性訪問地址一樣時的一種特殊情況。這種情況下,可以把常用的資料加cache來優化訪存。

空間區域性性(Spatial locality):

  如果某個位置的資訊被訪問,那和它相鄰的資訊也很有可能被訪問到。 這個也很好理解,我們大部分情況下程式碼都是順序執行,資料也是順序訪問的。

記憶體區域性性(Memory locality):

訪問記憶體時,大概率會訪問連續的塊,而不是單一的記憶體地址,其實就是空間區域性性在記憶體上的體現。目前計算機設計中,都是以塊/頁為單位管理排程儲存,其實就是在利用空間區域性性來優化效能。

分支區域性性(Branch locality)

  這個又被稱為順序區域性性,計算機中大部分指令是順序執行,順序執行和非順序執行的比例大致是5:1,即便有if這種選擇分支,其實大多數情況下某個分支都是被大概率選中的,於是就有了CPU的分支預測優化。

等距區域性性(Equidistant locality)

  等距區域性性是指如果某個位置被訪問,那和它相鄰等距離的連續地址極有可能會被訪問到,它位於空間區域性性和分支區域性性之間。 舉個例子,比如多個相同格式的資料陣列,你只取其中每個資料的一部分欄位,那麼他們可能在記憶體中地址距離是等距的,這個可以通過簡單的線性預測就預測是未來訪問的位置。

實際應用

  計算機領域關於區域性性非常多的利用,有很多你每天都會用到,但可能並沒有察覺,另外一些可能離你會稍微遠一些,接下來我們舉幾個例子來深入瞭解下區域性性的應用。

計算機儲存層級結構

極客時間
  上圖來自極客時間徐文浩的《深入淺出計算機組成原理》,我們以目前常見的普通家用電腦為例 ,分別說下上圖各級儲存的大小和訪問速度,資料來源於https://people.eecs.berkeley.edu/~rcs/research/interactive_latency.html
在這裡插入圖片描述
  從最快的L1 Cache到最慢的HDD,其兩者的訪存時間差距達到了6個數量級,即便是和記憶體比較,也有幾百倍的差距。舉個例子,如果CPU在運算是直接從記憶體中讀取指令和資料,執行一條指令0.3ns,然後從記憶體讀下一條指令,等120ns,這樣CPU 99%計算時間都會被浪費掉。但就是因為有區域性性的存在,每一層都只有少部分資料會被頻繁訪問,我們可以把這部分資料從底層儲存挪到高層儲存,可以降低大部分的資料讀取時間。
  
  可能有些人好奇,為什麼不把L1 快取做的大點,像記憶體那麼大,直接替代掉記憶體,不是效能更好嗎?雖然是這樣,但是L1 Cache單位價格要比記憶體單位的價格貴好多(大概差200倍),有興趣可以瞭解下DRAM和SRAM。
  我們可以通過編寫快取記憶體友好的程式碼邏輯來提升我們的程式碼效能,有兩個基本方法 。

  1. 讓最常見的情況執行的快,程式大部分的執行實際都花在少了核心函式上,而這些函式把大部分時間都花在少量迴圈上,把注意力放在這些程式碼上。
  2. 讓每個迴圈內快取不命中率最小。比如儘量不要列遍歷二維陣列。

MemCache

在這裡插入圖片描述
  MemCache在大型網站架構中經常看到。DB一般公司都會用mysql,即便是做了分庫分表,資料資料庫單機的壓力還是非常大的,這時候因為區域性性的存在,可能很多資料會被頻繁訪問,這些資料就可以被cache到像redis這種memcache中,當redis查不到資料,再去查db,並寫入redis。
  因為redis的水平擴充套件能力和簡單查詢能力要比mysql強多了,查起來也快。所以這種架構設計有幾個好處:

  1. 加快了資料查詢的平均速度。
  2. 大幅度減少DB的壓力。

    CDN

      CDN的全稱是Content Delivery Network,即內容分發網路(圖片來自百度百科) 。CDN常用於大的素材下發,比如圖片和視訊,你在淘寶上開啟一個圖片,這個圖片其實會就近從CDN機房拉去資料,而不是到阿里的機房拉資料,可以減少阿里機房的出口頻寬佔用,也可以減少使用者載入素材的等待時間。
    在這裡插入圖片描述
      CDN在網際網路中被大規模使用,像視訊、直播網站,電商網站,甚至是12306都在使用,這種設計對公司可以節省頻寬成本,對使用者可以減少素材載入時間,提升使用者體驗。看到這,有沒有發現,CDN的邏輯和Memcache的使用很類似,你可以直接當他是一個網際網路版的cache優化。

    Java JIT

  JIT全稱是Just-in-time Compiler,中文名為即時編譯器,是一種Java執行時的優化。Java的執行方式和C++不太一樣,因為為了實現write once, run anywhere的跨平臺需求,Java實現了一套位元組碼機制,所有的平臺都可以執行同樣的位元組碼,執行時有該平臺的JVM將位元組碼實時翻譯成該平臺的機器碼再執行。問題在於位元組碼每次執行都要翻譯一次,會很耗時。
  在這裡插入圖片描述
  圖片來自鄭雨迪Introduction to Graal ,Java 7引入了tiered compilation的概念,綜合了C1的高啟動效能及C2的高峰值效能。這兩個JIT compiler以及interpreter將HotSpot的執行方式劃分為五個級別:

  • level 0:interpreter解釋執行
  • level 1:C1編譯,無profiling
  • level 2:C1編譯,僅方法及迴圈back-edge執行次數的profiling
  • level 3:C1編譯,除level 2中的profiling外還包括branch(針對分支跳轉位元組碼)及receiver type(針對成員方法呼叫或類檢測,如checkcast,instnaceof,aastore位元組碼)的profiling
  • level 4:C2編譯

  通常情況下,一個方法先被解釋執行(level 0),然後被C1編譯(level 3),再然後被得到profile資料的C2編譯(level 4)。如果編譯物件非常簡單,虛擬機器認為通過C1編譯或通過C2編譯並無區別,便會直接由C1編譯且不插入profiling程式碼(level 1)。在C1忙碌的情況下,interpreter會觸發profiling,而後方法會直接被C2編譯;在C2忙碌的情況下,方法則會先由C1編譯並保持較少的profiling(level 2),以獲取較高的執行效率(與3級相比高30%)。
  這裡將少部分位元組碼實時編譯成機器碼的方式,可以提升java的執行效率。可能有人會問,為什麼不預先將所有的位元組碼編譯成機器碼,執行的時候不是更快更省事嗎?首先機器碼是和平臺強相關的,linux和unix就可能有很大的不同,何況是windows,預編譯會讓java失去誇平臺這種優勢。 其次,即時編譯可以讓jvm拿到更多的執行時資料,根據這些資料可以對位元組碼做更深層次的優化,這些是C++這種預編譯語言做不到的,所以有時候你寫出的java程式碼執行效率會比C++的高。

CopyOnWrite

  CopyOnWrite寫時複製,最早應該是源自linux系統,linux中在呼叫fork() 生成子程式時,子程式應該擁有和父程式一樣的指令和資料,可能子程式會修改一些資料,為了避免汙染父程式的資料,所以要給子程式單獨拷貝一份。出於效率考慮,fork時並不會直接複製,而是等到子程式的各段資料需要寫入才會複製一份給子程式,故此得名 寫時複製
  在計算機的世界裡,讀寫的分佈也是有很大的區域性性的,大多數情況下寫遠大於讀, 寫時複製 的方式,可以減少大量不必要的複製,提升效能。 另外這種方式也不僅僅是用在linux核心中,java的concurrent包中也提供了CopyOnWriteArrayList CopyOnWriteArraySet。像Spark中的RDD也是用CopyOnWrite來減少不必要的RDD生成。
  

處理

  上面列舉了那麼多區域性性的應用,其實還有很多很多,我只是列舉出了幾個我所熟知的應用,雖然上面這些例子,我們都利用區域性性得到了能效、成本上的提升。但有些時候它也會給我們帶來一些不好的體驗,更多的時候它其實就是一把雙刃劍,我們如何識別區域性性,利用它好的一面,避免它壞的一面?

識別

  文章開頭也說過,區域性性其實就是一種概率的不均等性,所以只要概率不均等就一定存在區域性性,因為很多時候這種概率不均太明顯了,非常好識別出來,然後我們對大頭做相應的優化就行了。但可能有些時候這種概率不均需要做很詳細的計算才能發現,最後還得核對成本才能考慮是否值得去做,這種需要具體問題具體分析了。    
  如何識別區域性性,很簡單,看概率分佈曲線,只要不是一條水平的直線,就一定存在區域性性。  

利用

  發現區域性性之後對我們而言是如何利用好這些區域性性,用得好提升效能、節約資源,用不好區域性性就會變成阻礙。而且不光是在計算機領域,區域性性在非計算機領域也可以利用。
##### 效能優化
  上面列舉到的很多應用其實就是通過區域性性做一些優化,雖然這些都是別人已經做好的,但是我們也可以參考其設計思路。
  恰巧最近我也在做我們一個java服務的效能優化,利用jstack、jmap這些java自帶的分析工具,找出其中最吃cpu的執行緒,找出最佔記憶體的物件。我發現有個redis資料查詢有問題,因為每次需要將一個大字串解析很多個鍵值對,中間會產生上千個臨時字串,還需要將字串parse成long和double。redis資料太多,不可能完全放的記憶體裡,但是這裡的key有明顯的區域性性,大量的查詢只會集中在頭部的一些key上,我用一個LRU Cache快取頭部資料的解析結果,就可以減少大量的查redis+解析字串的過程了。
  另外也發現有個程式碼邏輯,每次請求會被重複執行幾千次,耗費大量cpu,這種熱點程式碼,簡單幾行改動減少了不必要的呼叫,最終減少了近50%的CPU使用。
  
##### 非計算機領域
  《高能人士的七個習慣》裡提到了一種工作方式,將任務劃分為重要緊急、不重要但緊急、重要但不緊急、不重要不緊急四種,這種劃分方式其實就是按單位時間的重要度排序的,按單位時間的重要度越高收益越大。《The Effective Engineer》裡直接用leverage(槓桿率)來衡量每個任務的重要性。這兩種方法差不多是類似的,都是優先做高收益率的事情,可以明顯提升你的工作效率。
  這就是工作中收益率的區域性性導致的,只要少數事情有比較大的收益,才值得去做。還有一個很著名的法則__82法則__,在很多行業、很多領域都可以套用,80%的xxx來源於20%的xxx ,80%的工作收益來源於20%的工作任務,區域性性給我們的啟示“永遠關注最重要的20%” 。

避免

  上面我們一直在講如何通過區域性性來提升效能,但有時候我們需要避免區域性性的產生。 比如在大資料運算時,時常會遇到資料傾斜、資料熱點的問題,這就是資料分佈的區域性性導致的,資料傾斜往往會導致我們的資料計算任務耗時非常長,資料熱點會導致某些單節點成為整個叢集的效能瓶頸,但大部分節點卻很閒,這些都是我們需要極力避免的。
  一般我們解決熱點和資料切斜的方式都是提供過重新hash打亂整個資料讓資料達到均勻分佈,當然有些業務邏輯可能不會讓你隨意打亂資料,這時候就得具體問題具體分析了。感覺在大資料領域,區域性性極力避免,當然如果沒法避免你就得通過其他方式來解決了,比如HDFS中小檔案單節點讀的熱點,可以通過減少加副本緩解。其本質上沒有避免區域性性,只增加資源緩解熱點了,據說微博為應對明星出軌Redis叢集也是採取這種加資源的方式。
 

參考資料

  1. 維基百科區域性性原理
  2. 《計算機組成與設計》 David A.Patterson / John L.Hennessy
  3. 《深入淺出計算機組成原理》 極客時間 徐文浩
  4. 《深入理解計算機系統》 Randal E.Bryant / David O'Hallaron 龔奕利 / 雷迎春(譯)
  5. Interactive latencies
  6. Introduction to Graal 鄭雨迪

相關文章