CPU通俗演義及代碼級(jí)性能優(yōu)化實(shí)例分析
做任何事情要形成自己的方法體系,這樣在做事情的時(shí)候才能游刃有余。前面文章我們簡(jiǎn)單介紹了一個(gè)簡(jiǎn)單的例子,說(shuō)明了代碼開(kāi)發(fā)中應(yīng)該如何保證程序的性能。今天我們將更加深入的介紹如何在代碼層面提升程序的性能。并且我們總結(jié)為幾種情況,這樣在以后開(kāi)發(fā)中就可以套用。另外,本節(jié)我們主要介紹的是代碼級(jí)的性能優(yōu)化,關(guān)于涉及操作系統(tǒng)甚至整個(gè)分布式大系統(tǒng)的性能優(yōu)化我們另外單獨(dú)介紹。
程序是運(yùn)行在CPU之上的,因此在介紹性能優(yōu)化之前我們有必要介紹一下CPU的內(nèi)核結(jié)構(gòu)。在前文中我們對(duì)CPU進(jìn)行了簡(jiǎn)化處理(如圖1),實(shí)際上CPU的結(jié)構(gòu)非常復(fù)雜,畢竟一顆CPU由幾十億個(gè)晶體管組成的。
CPU通俗演義
CPU的作用很好理解,它就是一個(gè)數(shù)據(jù)加工部件。CPU就像一個(gè)大型的工廠,它將原料(數(shù)據(jù))加工成半成品和成品;而內(nèi)存則像一個(gè)大型的倉(cāng)庫(kù)。雖然CPU與內(nèi)存都在機(jī)箱中,但CPU訪問(wèn)內(nèi)存中的數(shù)據(jù)并不是非常方便,就好像工廠和大型倉(cāng)庫(kù)之間的距離,有幾百公里。從倉(cāng)庫(kù)向工廠運(yùn)算原材料需要用火車才行,運(yùn)輸一次材料可能要幾個(gè)小時(shí)。
在這個(gè)大型工廠(CPU)里面有很多東西,最為重要的就是車間(CPU核)、生產(chǎn)線(ALU)、物料暫存區(qū)(寄存器)、工廠小倉(cāng)庫(kù)(緩存)等內(nèi)容。為了更好理解上面這些內(nèi)容的關(guān)系,這里做了一個(gè)簡(jiǎn)化的平面圖。
工廠加工產(chǎn)品所需要的原材料需要從外面的大型倉(cāng)庫(kù)運(yùn)輸過(guò)來(lái)。由于從工廠外大型倉(cāng)庫(kù)到工廠的距離比較遠(yuǎn),耗時(shí)比較長(zhǎng),因此總是有計(jì)劃的,批量的將物料從工廠外的大倉(cāng)庫(kù)運(yùn)輸?shù)焦S內(nèi)的小倉(cāng)庫(kù)。工廠的車間突然需要一些原材料,那就只能讓火車重新跑一趟了。
運(yùn)輸過(guò)來(lái)的原材料不能亂放,否則要是下雨刮風(fēng)啥的不就損壞了。因此,原材料會(huì)被統(tǒng)一的放在工廠內(nèi)的小倉(cāng)庫(kù)(CPU緩存)里面,各個(gè)車間根據(jù)需要從小倉(cāng)庫(kù)運(yùn)輸原材料到車間。
在車間有個(gè)暫存區(qū)(寄存器),用于存放從小倉(cāng)庫(kù)運(yùn)過(guò)來(lái)的材料。當(dāng)然暫存區(qū)除了放原材料之外,還會(huì)放一些半成品和成品。車間有車間的秩序,不能亂放,否則會(huì)出問(wèn)題的。暫存區(qū)是很有必要的,要不然需要點(diǎn)材料就去倉(cāng)庫(kù)拿,那不得類似工人啊。
有了原材料之后,工人就可以將原材料放到生產(chǎn)線(ALU)上進(jìn)行生產(chǎn)了。生產(chǎn)完成的成品又會(huì)放回暫存區(qū),然后運(yùn)輸出去。暫存區(qū)和生產(chǎn)線都在車間里面,搬運(yùn)原材料和成品都很快,幾乎是一兩分鐘就可以完成。
關(guān)于流水線
為了提高產(chǎn)品生產(chǎn)速度,在一個(gè)車間里面通常是有多個(gè)生產(chǎn)線的。每一個(gè)生產(chǎn)線的大概流程是運(yùn)輸原材料、原材料預(yù)處理(比如撕掉包裝或者切成小塊等)、原材料加工和成品運(yùn)回暫存區(qū)等步驟。CPU也是有類似的流水線的,任何一個(gè)指令都要經(jīng)過(guò)讀取指令、譯碼、執(zhí)行和寫(xiě)回(寄存器或者內(nèi)存)等。
以一個(gè)生產(chǎn)黃桃罐頭的車間為例,在這個(gè)車間里面要同時(shí)生產(chǎn)罐頭瓶、罐頭瓶蓋和糖水黃桃(這里是假設(shè),實(shí)際工廠不是這樣)。因此,也就有生產(chǎn)罐頭瓶的流水線、生產(chǎn)罐頭瓶蓋的流水線和生產(chǎn)糖水黃桃的流水線。通常我們安排的流程是先生產(chǎn)罐頭瓶和瓶蓋,這樣生產(chǎn)的糖水黃桃就可以裝瓶完成成品了。但是,有的時(shí)候可能運(yùn)送暫存區(qū)或者工廠的小倉(cāng)庫(kù)沒(méi)有玻璃了,這樣就沒(méi)法生產(chǎn)罐頭瓶。不過(guò)沒(méi)關(guān)系,車間還是可以先生產(chǎn)糖水黃桃的,生產(chǎn)完之后先放到暫存區(qū),等什么時(shí)候罐頭瓶生產(chǎn)完之后在裝瓶。
上面這個(gè)流程其實(shí)就是所謂的指令亂序。也就是CPU在執(zhí)行指令的時(shí)候并不是按我們寫(xiě)代碼的順序執(zhí)行的,而可能會(huì)打亂順序。比如下面這段代碼,由于兩行代碼之間沒(méi)有任何依賴,因此在CPU中可能會(huì)先執(zhí)行b=2,后執(zhí)行a=1。
- int a = 1;
- int b = 2;
存儲(chǔ)的金字塔結(jié)構(gòu)
另外一個(gè)比較重要的知識(shí)點(diǎn)是需要知道軟件開(kāi)發(fā)涉及的存儲(chǔ)金字塔。具體如圖所示,其中寄存器、一級(jí)緩存、二級(jí)緩存和三級(jí)緩存是CPU內(nèi)部的部件,然后是內(nèi)存和磁盤(pán)。最后是遠(yuǎn)程存儲(chǔ),比如SAN、NAS或者對(duì)象存儲(chǔ)或者云計(jì)算中的云盤(pán)等存儲(chǔ)都屬于遠(yuǎn)程存儲(chǔ)。
通常來(lái)說(shuō)越往金字塔底部,容量越大,但延遲也越大,性能越差。這里面有個(gè)特例,就是本地存儲(chǔ)和遠(yuǎn)程存儲(chǔ),如果遠(yuǎn)程存儲(chǔ)采用的介質(zhì)與本地相同,則肯定遠(yuǎn)程存儲(chǔ)性能要差一些。但當(dāng)前有些分布式存儲(chǔ),通信鏈路采用RDMA,存儲(chǔ)介質(zhì)采用SSD,那么本地的機(jī)械磁盤(pán)就要比遠(yuǎn)程存儲(chǔ)性能差了。
了解了這個(gè)結(jié)構(gòu)之后,我們總結(jié)一下。其實(shí)性能問(wèn)題總結(jié)起來(lái)就是一句話,盡量少的使用計(jì)算資源(比如不同的排序算法),盡量多的用金字塔頂部的部件存儲(chǔ)要訪問(wèn)數(shù)據(jù)(比如文件系統(tǒng)的緩存)。
程序性能分析工具
正所謂:“欲善其事,必先利其器”。因此,要想進(jìn)行性能優(yōu)化,自然需要有相應(yīng)的工具進(jìn)行分析。本文僅針對(duì)Linux操作系統(tǒng)進(jìn)行介紹,其它操作系統(tǒng)實(shí)在是不熟悉。在Linux操作系統(tǒng)下面,用的最多的性能分析工具恐怕非top莫屬。
(1) top命令
top命令可以實(shí)時(shí)的觀察進(jìn)程的計(jì)算資源使用情況(CPU利用率)和整個(gè)系統(tǒng)的綜合負(fù)載。如圖是我們通過(guò)一個(gè)Python腳本模擬高負(fù)債程序,可以看到起CPU利用率已經(jīng)達(dá)到100%。
top工具可以幫助我們分析高度消耗計(jì)算資源的程序的性能。另外還有其它性能分析工具,比如ps、vmstat、mpstat和prstat等等。工具比較多,限于篇幅問(wèn)題,本文暫時(shí)就不做介紹了。
性能優(yōu)化方法總結(jié)
有了前面的準(zhǔn)備知識(shí)后,下面我們進(jìn)入正題。本節(jié)內(nèi)容總結(jié)了在程序代碼級(jí)別常見(jiàn)的問(wèn)題,并結(jié)合實(shí)例給出了解決方法,下面我們逐個(gè)分析一下。
1. 優(yōu)化程序代碼結(jié)構(gòu)
這種問(wèn)題的原因在于程序代碼結(jié)構(gòu)不合理,導(dǎo)致過(guò)度使用計(jì)算資源。如果往高大上的說(shuō),那就是算法不行。比如下面兩段程序,前一段程序在for循環(huán)的條件判斷中有一個(gè)strlen調(diào)用,用于判斷字符串的長(zhǎng)度。而后一段代碼則將strlen移到條件判斷外面。
如果字符串大的情況下,這兩個(gè)程序的性能差異可能有百倍。這個(gè)主要是因?yàn)閟trlen函數(shù)其實(shí)是個(gè)循環(huán)判斷,需要消耗大量的計(jì)算資源。
另外一種最為常見(jiàn)例子是關(guān)于排序算法的,比如冒泡排序的性能比快速排序差。因?yàn)閮烧哂?jì)算量(時(shí)間復(fù)雜度)不同,所以算法的性能自然就不同。
2. 運(yùn)算符合理選擇
這部分也是針對(duì)計(jì)算資源消耗的優(yōu)化。在介紹這部分內(nèi)容之前,我們腦子里要有個(gè)概念。就是不同的運(yùn)算(加減乘除)消耗的計(jì)算資源是不同的,其中加減、位運(yùn)算和移位操作最低,可以認(rèn)為是1,那么乘法則是3-4,而除法則大概是10-30左右。
了解上上面的內(nèi)容之后,那我們?cè)诔绦蜷_(kāi)發(fā)中就要盡量少用除法運(yùn)算,因?yàn)樗男詢r(jià)比實(shí)在不高(太消耗計(jì)算資源)。有人可能會(huì)想怎么可能?有的時(shí)候就要用除法怎么辦?下面我們看一個(gè)例子,這個(gè)例子是JDK中關(guān)于Hashmap的實(shí)現(xiàn)。
Hashmap是通過(guò)哈希表實(shí)現(xiàn)的,哈希表的概念這里就不啰嗦了。在查找或者存儲(chǔ)的時(shí)候需要根據(jù)Key值取模,定位元素的位置。通常我們能想到的方法就是取模的運(yùn)算符(計(jì)算量類似除法),但在Hashmap中卻沒(méi)有用取模運(yùn)算符,而是用的位運(yùn)算。這樣,整個(gè)性能就會(huì)有十倍以上的提升,如下是它的代碼。
- static int indexFor(int h, int length) {
- return h & (length-1);
- }
3. 減少對(duì)內(nèi)存的訪問(wèn)
通過(guò)前面的準(zhǔn)備知識(shí)我們知道內(nèi)存的訪問(wèn)比寄存器慢100倍,因此在寫(xiě)代碼的時(shí)候盡量減少對(duì)內(nèi)存的訪問(wèn)。那么怎么減少對(duì)內(nèi)存的訪問(wèn)呢?我們?nèi)匀豢匆粋€(gè)例子,比如一個(gè)簡(jiǎn)單的累加運(yùn)算(這個(gè)例子比較極端)。前者是通過(guò)全局變量存儲(chǔ)累加和,而后者是通過(guò)局部變量。
為了深入的了解兩者差異,我們需要對(duì)程序進(jìn)行反匯編,然后對(duì)比一下反匯編代碼。對(duì)比上線代碼可以看出前者每次計(jì)算都有訪問(wèn)多次內(nèi)存(其中帶圓括弧的匯編語(yǔ)句),而后者則將其轉(zhuǎn)換成了寄存器訪問(wèn)。
雖然我們通常認(rèn)為局部變量在函數(shù)棧中(內(nèi)存空間),但實(shí)際上編譯器在進(jìn)行程序編譯的時(shí)候會(huì)對(duì)代碼進(jìn)行優(yōu)化,將局部變量?jī)?yōu)化為寄存器。因此,我們?cè)趯?shí)際開(kāi)發(fā)的時(shí)候盡量使用局部變量,減少對(duì)內(nèi)存的訪問(wèn)。
4. 減少對(duì)磁盤(pán)的訪問(wèn)
道理跟前面一個(gè)是一樣的,還是那個(gè)存儲(chǔ)金字塔。如果你的程序有很多對(duì)磁盤(pán)的訪問(wèn),性能通常不會(huì)好到那去。通常的方法是使用內(nèi)存作為緩存。在磁盤(pán)方面性能優(yōu)化最經(jīng)典的例子恐怕就是文件系統(tǒng)的頁(yè)緩存了。也就是文件系統(tǒng)寫(xiě)入的數(shù)據(jù)不會(huì)馬上寫(xiě)到磁盤(pán),而是先寫(xiě)到緩存(內(nèi)存)中。而讀數(shù)據(jù)的時(shí)候則是通過(guò)預(yù)讀機(jī)制提前將數(shù)據(jù)讀入內(nèi)存,文件系統(tǒng)從內(nèi)存讀數(shù)據(jù),而不是從磁盤(pán)。由于內(nèi)存的性能是機(jī)械磁盤(pán)的十萬(wàn)倍以上,因此文件系統(tǒng)的性能得以大大提高(這里有場(chǎng)景限制,我們后面再詳細(xì)介紹)。
另外一個(gè)經(jīng)典案例還是文件系統(tǒng)相關(guān)的,這個(gè)就是Linux的虛擬文件系統(tǒng)(VFS)。我們知道文件系統(tǒng)每個(gè)文件都對(duì)應(yīng)著一個(gè)inode,而inode也是存儲(chǔ)在磁盤(pán)上的。如果我們要打開(kāi)一個(gè)文件,首先需要從磁盤(pán)找到inode,然后讀取到內(nèi)存,然后才能進(jìn)行后續(xù)的讀寫(xiě)操作。
在VFS中,在文件打開(kāi)的時(shí)候,VFS會(huì)將inode放入一個(gè)內(nèi)存中的哈希表中,而且在關(guān)閉文件的情況下并不釋放。這樣,當(dāng)應(yīng)用程序再次打開(kāi)文件的時(shí)候就可以直接從內(nèi)存找到該inode,而不用重新讀磁盤(pán)了。
上面這些都是特例,大家要融會(huì)貫通,希望對(duì)大家的軟件設(shè)計(jì)有所幫助。最后,性能優(yōu)化本質(zhì),還是那一句話,盡量少的使用計(jì)算資源,盡量多的用金字塔頂部的部件存儲(chǔ)要訪問(wèn)數(shù)據(jù)。