學過計算機底層原理、瞭解過不少架構設計或者是作過優化的同窗,應該很熟悉局部性原理。即使是非計算機行業的人,在作各類調優、提效時也不得不考慮到局部性,只不過他們不經常使用局部性一詞。若是抽象程度再高一些,甚至能夠說地球、生命、萬事萬物都是局部性的產物,由於這些都是宇宙中熵分佈佈局、局部的熵低致使的,若是宇宙中到處熵一致,有的只有一篇混沌。
因此什麼是 局部性 ?這是一個經常使用的計算機術語,是指處理器在訪問某些數據時短期內存在重複訪問,某些數據或者位置訪問的機率極大,大多數時間只訪問_局部_的數據。基於局部性原理,計算機處理器在設計時作了各類優化,好比現代CPU的多級Cache、分支預測…… 有良好局部性的程序比局部性差的程序運行得更快。雖然局部性一詞源於計算機設計,但在當今分佈式系統、互聯網技術裏也不乏局部性,好比像用redis這種memcache來減輕後端的壓力,CDN作素材分發減小帶寬佔用率……
局部性的本質是什麼?其實就是機率的不均等,這個宇宙中,不少東西都不是平均分佈的,平均分佈是機率論中幾何分佈的一種特殊形式,很是簡單,但世界就是沒這麼簡單。咱們更長聽到的發佈叫作高斯發佈,同時也被稱爲正態分佈,由於它就是正常狀態下的機率發佈,起機率圖以下,但這個也不是今天要說的。
其實有不少狀況,不少事物有很強的頭部集中現象,能夠用機率論中的泊松分佈來刻畫,這就是局部性在機率學中的刻畫形式。
上面分別是泊松分佈的示意圖和機率計算公式,$\lambda$ 表示單位時間(或單位面積)內隨機事件的平均發生次數,$e$表示天然常數2.71828..,k表示事件發生的次數。要注意在刻畫局部性時$\lambda$表示不命中高頻數據的頻度,$\lambda$越小,頭部集中現象越明顯。html
局部性有兩種基本的分類, 時間局部性 和 空間局部性 ,按Wikipedia的資料,能夠分爲如下五類,其實有些就是時間局部性和空間局部性的特殊狀況。java
若是某個信息此次被訪問,那它有可能在不久的將來被屢次訪問。時間局部性是空間局部性訪問地址同樣時的一種特殊狀況。這種狀況下,能夠把經常使用的數據加cache來優化訪存。mysql
若是某個位置的信息被訪問,那和它相鄰的信息也頗有可能被訪問到。 這個也很好理解,咱們大部分狀況下代碼都是順序執行,數據也是順序訪問的。linux
訪問內存時,大機率會訪問連續的塊,而不是單一的內存地址,其實就是空間局部性在內存上的體現。目前計算機設計中,都是以塊/頁爲單位管理調度存儲,其實就是在利用空間局部性來優化性能。git
這個又被稱爲順序局部性,計算機中大部分指令是順序執行,順序執行和非順序執行的比例大體是5:1,即使有if這種選擇分支,其實大多數狀況下某個分支都是被大機率選中的,因而就有了CPU的分支預測優化。github
等距局部性是指若是某個位置被訪問,那和它相鄰等距離的連續地址極有可能會被訪問到,它位於空間局部性和分支局部性之間。 舉個例子,好比多個相同格式的數據數組,你只取其中每一個數據的一部分字段,那麼他們可能在內存中地址距離是等距的,這個能夠經過簡單的線性預測就預測是將來訪問的位置。redis
計算機領域關於局部性很是多的利用,有不少你天天都會用到,但可能並無察覺,另一些可能離你會稍微遠一些,接下來咱們舉幾個例子來深刻了解下局部性的應用。sql
上圖來自極客時間徐文浩的《深刻淺出計算機組成原理》,咱們以目前常見的普通家用電腦爲例 ,分別說下上圖各級存儲的大小和訪問速度,數據來源於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。
咱們能夠經過編寫高速緩存友好的代碼邏輯來提高咱們的代碼性能,有兩個基本方法 。數據庫
MemCache在大型網站架構中常常看到。DB通常公司都會用mysql,即使是作了分庫分表,數據數據庫單機的壓力仍是很是大的,這時候由於局部性的存在,可能不少數據會被頻繁訪問,這些數據就能夠被cache到像redis這種memcache中,當redis查不到數據,再去查db,並寫入redis。
由於redis的水平擴展能力和簡單查詢能力要比mysql強多了,查起來也快。因此這種架構設計有幾個好處:windows
大幅度減小DB的壓力。
CDN的全稱是Content Delivery Network,即內容分發網絡(圖片來自百度百科) 。CDN經常使用於大的素材下發,好比圖片和視頻,你在淘寶上打開一個圖片,這個圖片其實會就近從CDN機房拉去數據,而不是到阿里的機房拉數據,能夠減小阿里機房的出口帶寬佔用,也能夠減小用戶加載素材的等待時間。
CDN在互聯網中被大規模使用,像視頻、直播網站,電商網站,甚至是12306都在使用,這種設計對公司能夠節省帶寬成本,對用戶能夠減小素材加載時間,提高用戶體驗。看到這,有沒有發現,CDN的邏輯和Memcache的使用很相似,你能夠直接當他是一個互聯網版的cache優化。
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),而後被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寫時複製,最先應該是源自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集羣也是採起這種加資源的方式。