8000字講清楚程序性能優(yōu)化。
本文聊一個(gè)程序員都會(huì)關(guān)注的問(wèn)題:性能。
當(dāng)大家談到“性能”時(shí),你首先想到的會(huì)是什么?
- 是每次請(qǐng)求需要多長(zhǎng)時(shí)間才能返回?
- 是每秒鐘能夠處理多少次請(qǐng)求?
- 還是程序的CPU和內(nèi)存使用率高不高?
這些問(wèn)題基本上反應(yīng)了性能關(guān)注的幾個(gè)主要方面:響應(yīng)時(shí)間、吞吐量和資源利用率。在這三個(gè)方面中,如果能夠?qū)崿F(xiàn)更低的響應(yīng)時(shí)間和更高的吞吐量,那么資源利用率也必然得到優(yōu)化。這是因?yàn)槲覀兊墓ぷ骺偸窃谟邢薜挠布?、軟件、時(shí)間和預(yù)算等的約束下進(jìn)行的,而優(yōu)化前兩個(gè)方面將有助于更有效地利用這些資源。
因此,本文將主要圍繞響應(yīng)時(shí)間和吞吐量的優(yōu)化展開(kāi)介紹,包括相關(guān)領(lǐng)域的定義和軟硬件方面的優(yōu)化方法。
響應(yīng)時(shí)間
想象一下,你在餐廳點(diǎn)了一道菜,響應(yīng)時(shí)間就是從你下單到菜品送到你面前的這段時(shí)間。
在計(jì)算機(jī)里,它指的是單次請(qǐng)求或指令處理的時(shí)間。
1.1 軟件層面的優(yōu)化
軟件層面的優(yōu)化主要是通過(guò)減少非必要的處理來(lái)降低響應(yīng)時(shí)間,包括減少I(mǎi)O請(qǐng)求和優(yōu)化代碼邏輯。
1.1.1 減少I(mǎi)O請(qǐng)求
減少I(mǎi)O請(qǐng)求的意義
IO就是輸入輸出,減少I(mǎi)O處理就是減少對(duì)輸入輸出設(shè)備的訪(fǎng)問(wèn)。在計(jì)算機(jī)中,除了CPU和內(nèi)存,其它的鍵盤(pán)、鼠標(biāo)、顯示器、音響、硬盤(pán)、網(wǎng)卡等等都屬于輸入輸出設(shè)備。減少對(duì)這些設(shè)備的訪(fǎng)問(wèn)為什么有用呢?
首先讓我們了解下程序的運(yùn)行過(guò)程,大概是這樣的:
操作系統(tǒng)首先將程序的二進(jìn)制指令從硬盤(pán)加載到內(nèi)存,然后再?gòu)膬?nèi)存加載到CPU,然后CPU就按照二進(jìn)制指令的邏輯進(jìn)行處理,指令是加減乘除的就做加減乘除,指令是跳轉(zhuǎn)的就做跳轉(zhuǎn),指令是訪(fǎng)問(wèn)遠(yuǎn)程網(wǎng)絡(luò)的就通過(guò)操作系統(tǒng)+網(wǎng)卡發(fā)起網(wǎng)絡(luò)請(qǐng)求,指令是訪(fǎng)問(wèn)文件的就通過(guò)操作系統(tǒng)+硬盤(pán)進(jìn)行文件讀寫(xiě)。
在CPU的這些處理中,邏輯判斷、跳轉(zhuǎn)、加減乘除都是很快的,因?yàn)樗鼈冎辉贑PU內(nèi)部進(jìn)行處理,CPU中每條指令的運(yùn)行時(shí)間極短;但是如果要進(jìn)行網(wǎng)絡(luò)請(qǐng)求、文件讀寫(xiě),速度就會(huì)大幅下降,這里邊有很多的損耗,包括系統(tǒng)調(diào)用的時(shí)間消耗、總線(xiàn)的傳輸時(shí)間消耗、IO設(shè)備的處理時(shí)間消耗、遠(yuǎn)程過(guò)程的處理時(shí)間消耗等。
我們可以通過(guò)一組數(shù)字直觀感受下,假設(shè)CPU的主頻是1GHZ,執(zhí)行1條指令需要1個(gè)時(shí)鐘周期,那么執(zhí)行1條指令的時(shí)間就是1納秒。而從硬盤(pán)讀取數(shù)據(jù)的時(shí)間消耗要遠(yuǎn)大于此,如果是機(jī)械硬盤(pán),大概在5-20毫秒,百萬(wàn)倍的差距;如果換成固態(tài)硬盤(pán),情況會(huì)好點(diǎn),普遍都在0.1毫秒以下,部分能達(dá)到微秒級(jí),但也是萬(wàn)倍、十萬(wàn)倍以上的差距。
所以減少I(mǎi)O請(qǐng)求能極大的降低響應(yīng)時(shí)間。那么我們可以采取什么方法呢?
硬盤(pán)IO的優(yōu)化
包括降低硬盤(pán)讀寫(xiě)頻次和采用順序讀寫(xiě)。
對(duì)于頻繁使用的數(shù)據(jù),根據(jù)業(yè)務(wù)情況,我們可以把它們放到內(nèi)存中,后續(xù)都從內(nèi)存讀取,速度會(huì)快上不少;對(duì)于需要寫(xiě)入硬盤(pán)的數(shù)據(jù),根據(jù)業(yè)務(wù)情況,我們可以先在內(nèi)存中攢幾條,達(dá)到一定的數(shù)據(jù)量之后再寫(xiě)入硬盤(pán),其實(shí)操作系統(tǒng)本身就會(huì)緩存寫(xiě)入,很多語(yǔ)言寫(xiě)數(shù)據(jù)到硬盤(pán)之后需要做一個(gè)flush的操作,就是用來(lái)實(shí)現(xiàn)最終寫(xiě)入硬盤(pán)的。我們使用Memcached、Redis等都是這個(gè)方案的延伸,只不過(guò)它們被封裝成了獨(dú)立的遠(yuǎn)程服務(wù)。
采用順序讀寫(xiě)主要針對(duì)的是機(jī)械硬盤(pán),因?yàn)闄C(jī)械硬盤(pán)挪動(dòng)懸臂和磁頭比較耗時(shí),順序讀寫(xiě)可以盡量減少機(jī)械移動(dòng)情況的發(fā)生,進(jìn)而提升讀寫(xiě)速度。
網(wǎng)絡(luò)IO的優(yōu)化
網(wǎng)絡(luò)IO的延遲一般都要在毫秒級(jí)以上,對(duì)網(wǎng)絡(luò)IO的優(yōu)化,除了類(lèi)似硬盤(pán)IO優(yōu)化中的的降低IO頻次,另外還可以通過(guò)使用更高效的傳輸協(xié)議、降低數(shù)據(jù)傳輸量等方式進(jìn)行優(yōu)化。
對(duì)于需要頻繁通過(guò)網(wǎng)絡(luò)獲取的數(shù)據(jù),比如訪(fǎng)問(wèn)遠(yuǎn)程服務(wù)、數(shù)據(jù)庫(kù)等獲取的數(shù)據(jù),我們可以在本地內(nèi)存進(jìn)行緩存,訪(fǎng)問(wèn)內(nèi)存比訪(fǎng)問(wèn)網(wǎng)絡(luò)要快很多,只需要選擇合適的緩存存活時(shí)間就好了。
對(duì)于短時(shí)間內(nèi)大量需要通過(guò)網(wǎng)絡(luò)獲取的數(shù)據(jù),我們可以采用批量獲取的方式,比如有一個(gè)列表,列表中的每一條數(shù)據(jù)都要調(diào)用接口去獲取某個(gè)相同的字段,列表有100條數(shù)據(jù)就要請(qǐng)求網(wǎng)絡(luò)100次,如果批量獲取,則只需要一次網(wǎng)絡(luò)請(qǐng)求就把100條數(shù)據(jù)全部拿到,這可以避免大量的網(wǎng)絡(luò)IO時(shí)間消耗,顯著降低業(yè)務(wù)處理的響應(yīng)時(shí)間。
我們一般認(rèn)為http是無(wú)狀態(tài)的,但是它的底層是基于TCP協(xié)議的,這樣每次發(fā)起http請(qǐng)求時(shí),網(wǎng)絡(luò)底層還是要先建立一個(gè)TCP連接,然后本次http請(qǐng)求結(jié)束后再釋放這個(gè)連接。你可能聽(tīng)說(shuō)過(guò)TCP的三次握手、四次揮手、TCP包的順序保證等,這些都需要在客戶(hù)端和服務(wù)器端來(lái)回多次通信才能完成,而且導(dǎo)致http的網(wǎng)絡(luò)請(qǐng)求效率不高。很多大佬也看到了這個(gè)問(wèn)題,所以搞出來(lái)了http2、http3,讓http使用長(zhǎng)連接、跑在UDP協(xié)議上。我們?cè)诰幊虝r(shí)選擇基于http2或者h(yuǎn)ttp3的通信庫(kù)就可以降低網(wǎng)絡(luò)延遲,如果有必要我們也可以直接使用TCP或者UDP編寫(xiě)網(wǎng)絡(luò)程序。
另外如果我們只需要網(wǎng)絡(luò)接口返回的部分?jǐn)?shù)據(jù),就沒(méi)必要傳輸完整的數(shù)據(jù),數(shù)據(jù)在網(wǎng)絡(luò)底層通過(guò)分包、分幀的方式進(jìn)行傳輸,數(shù)據(jù)越大,包、幀的數(shù)量越多,傳輸消耗的時(shí)間也越長(zhǎng)。對(duì)于減少數(shù)據(jù)傳輸量,除了業(yè)務(wù)上的約定,我們也可以通過(guò)一些序列化方式進(jìn)行優(yōu)化,比如采用Protobuf替代JSON通??梢陨筛痰南?nèi)容。
談到Protobuf,不得不提一下gRPC,它使用了Protobuf進(jìn)行序列化處理,還使用了更新的http協(xié)議,根據(jù)我的不嚴(yán)格測(cè)試,同樣的服務(wù),相比HTTP+JSON的方式,gRPC的網(wǎng)絡(luò)延遲可以降低1個(gè)數(shù)量級(jí)。
內(nèi)存IO的優(yōu)化
我們?cè)谏线叺姆治鲋惺前袰PU和內(nèi)存看做一個(gè)整體的,其實(shí)它們內(nèi)部的通信延遲也不可忽視,我們也有一些方法來(lái)優(yōu)化對(duì)內(nèi)存的訪(fǎng)問(wèn),包括下邊一些技術(shù):
零拷貝:這種技術(shù)要解決的問(wèn)題是數(shù)據(jù)在內(nèi)核態(tài)和用戶(hù)態(tài)之間的重復(fù)存放問(wèn)題。
什么是內(nèi)核態(tài)和用戶(hù)態(tài)?操作系統(tǒng)有兩個(gè)主要的功能,一是管理計(jì)算機(jī)上的所有軟件程序,主要是CPU和內(nèi)存資源的分配,二是為一些基礎(chǔ)計(jì)算能力提供統(tǒng)一的使用接口,比如網(wǎng)絡(luò)、硬盤(pán)這種;為了實(shí)現(xiàn)這兩大能力,操作系統(tǒng)就需要有一些管理程序來(lái)處理這些事,這些基礎(chǔ)管理程序就運(yùn)行在內(nèi)核態(tài),而操作系統(tǒng)上的其它程序則運(yùn)行在用戶(hù)態(tài)。同時(shí)基于安全考慮,內(nèi)核態(tài)的程序以及它們使用的資源必須要保護(hù)好,不能隨便訪(fǎng)問(wèn),所以?xún)?nèi)核態(tài)的數(shù)據(jù)就不能讓用戶(hù)態(tài)直接訪(fǎng)問(wèn),用戶(hù)程序訪(fǎng)問(wèn)相關(guān)數(shù)據(jù)時(shí)得先復(fù)制到用戶(hù)態(tài)。
舉個(gè)例子,當(dāng)程序從硬盤(pán)讀取數(shù)據(jù)時(shí),程序先要調(diào)用內(nèi)核程序,內(nèi)核程序再去訪(fǎng)問(wèn)硬盤(pán),此時(shí)數(shù)據(jù)先讀到內(nèi)核內(nèi)存空間中,然后再拷貝到用戶(hù)程序定義的內(nèi)存空間中。如果我們能把用戶(hù)態(tài)和內(nèi)核態(tài)之間的拷貝去掉,就是零拷貝技術(shù)了。
很多語(yǔ)言和框架中都提供了這種零拷貝的能力。比如Java中的Netty框架在發(fā)送文件時(shí),可以直接在內(nèi)核態(tài)將文件數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)端口,而不需要先一點(diǎn)點(diǎn)讀到用戶(hù)態(tài),再一點(diǎn)點(diǎn)寫(xiě)到內(nèi)核態(tài)的網(wǎng)絡(luò)處理程序。
其實(shí)Netty還使用了一些非傳統(tǒng)的零拷貝技術(shù),這包括直接內(nèi)存和復(fù)合緩沖。直接內(nèi)存是Java程序向操作系統(tǒng)直接申請(qǐng)一塊內(nèi)存,這塊內(nèi)存的數(shù)據(jù)可以直接與底層網(wǎng)絡(luò)傳輸進(jìn)行交互,不需要在內(nèi)核態(tài)和用戶(hù)態(tài)之間進(jìn)行拷貝。復(fù)合緩沖是Netty定義了一個(gè)邏輯上的大緩沖區(qū),把網(wǎng)絡(luò)傳輸中的多段小數(shù)據(jù)組合在一起,外部讀取數(shù)據(jù)時(shí)只需要和它打交道,這樣比較優(yōu)雅,實(shí)際也沒(méi)有產(chǎn)生內(nèi)存拷貝。
CPU緩存行:這種技術(shù)主要是充分利用CPU緩存,減少CPU對(duì)內(nèi)存的訪(fǎng)問(wèn)。
什么是CPU緩存?在上邊談到IO設(shè)備的訪(fǎng)問(wèn)時(shí)間時(shí),我們說(shuō)到硬盤(pán)的訪(fǎng)問(wèn)時(shí)間是CPU執(zhí)行單條指令消耗時(shí)間的萬(wàn)倍以上,其實(shí)內(nèi)存的訪(fǎng)問(wèn)時(shí)間相較CPU內(nèi)部也有百倍左右的差距,所以CPU中搞了一個(gè)緩存,將最近需要的數(shù)據(jù)或指令先加載到緩存中,后續(xù)執(zhí)行的時(shí)候都通過(guò)緩存進(jìn)行讀寫(xiě)。緩存與內(nèi)存相比,速度更快,訪(fǎng)問(wèn)一次可能只需要若干納秒,但是成本也更高,數(shù)量比較少,所以沒(méi)有完全代替內(nèi)存。
我們要減少內(nèi)存的訪(fǎng)問(wèn),就需要數(shù)據(jù)在CPU緩存中維持的時(shí)間更久。CPU緩存是以行為基本單位的,如果一行中保存了多個(gè)變量的數(shù)據(jù),它們可能就會(huì)相互影響,比如其中一個(gè)變量更新的頻率很高,這個(gè)緩存行可能就會(huì)頻繁失效,導(dǎo)致和內(nèi)存的頻繁同步,而這個(gè)緩存行中的另一個(gè)變量就受到了連帶影響。解決這個(gè)問(wèn)題可以讓變量獨(dú)占一個(gè)緩存行,比如前后使用一些空位進(jìn)行填充,Java中的Disruptor庫(kù)就是采用了這種方案。
綁定CPU與內(nèi)存:這種技術(shù)主要解決CPU或者內(nèi)核訪(fǎng)問(wèn)不同內(nèi)存時(shí)的速度差異問(wèn)題。
我們知道計(jì)算機(jī)中可以安裝多塊CPU、多條內(nèi)存,在同一塊CPU中也可能存在多個(gè)核心,也就是俗稱(chēng)的多核處理器,這些CPU、核心到每條內(nèi)存的距離可能是不相同的,CPU訪(fǎng)問(wèn)距離近的內(nèi)存,速度就會(huì)快些,訪(fǎng)問(wèn)距離遠(yuǎn)的內(nèi)存,速度就會(huì)慢些。對(duì)于計(jì)算機(jī)的使用者來(lái)說(shuō),我們肯定是希望程序的運(yùn)行速度越快越好,即使做不到最快,也不希望程序時(shí)快時(shí)慢,這樣容易導(dǎo)致?lián)矶隆?/span>
為了解決這個(gè)問(wèn)題,計(jì)算機(jī)發(fā)展出了一種稱(chēng)為NUMA的技術(shù),NUMA的全稱(chēng)是非一致性?xún)?nèi)存訪(fǎng)問(wèn)。在這種技術(shù)中,CPU和內(nèi)存劃分了不同的區(qū)域,CPU訪(fǎng)問(wèn)本區(qū)域的內(nèi)存時(shí)可以直接訪(fǎng)問(wèn),速度十分快;CPU訪(fǎng)問(wèn)其它區(qū)域的內(nèi)存時(shí)需要通過(guò)內(nèi)部的通道,速度會(huì)相對(duì)慢一些。然后操作系統(tǒng)可以感知到NUMA的區(qū)域分布情況,并提供了相應(yīng)的API,讓?xiě)?yīng)用程序可以將自己使用的內(nèi)存和CPU盡量保持在同一個(gè)區(qū)域,或者盡量平均分布在不同的區(qū)域,從而保證了CPU和內(nèi)存之間訪(fǎng)問(wèn)速度。
1.1.2 優(yōu)化代碼邏輯
在我的編程生涯早期,優(yōu)化程序性能時(shí)考慮最多的是:這里是不是可以少些幾行代碼,那里是不是可以不使用循環(huán)。這些固然可以?xún)?yōu)化程序的性能,但是遠(yuǎn)沒(méi)有上邊提到的優(yōu)化IO帶來(lái)的收益大。因?yàn)樯賵?zhí)行幾條代碼只是若干納秒的節(jié)省,而少一次IO則是百倍、千倍、萬(wàn)倍的節(jié)省。不過(guò)當(dāng)IO沒(méi)得優(yōu)化的時(shí)候,我們也不得不考慮在代碼邏輯上下下功夫,特別是一些計(jì)算密集型的程序。
可以從以下幾個(gè)方面著手優(yōu)化:
- 算法優(yōu)化:選擇更有效率的算法來(lái)減少計(jì)算時(shí)間。例如,使用快速排序代替冒泡排序,數(shù)據(jù)越多,快速排序的算法效率越高,節(jié)省的時(shí)間也越多。
- 數(shù)據(jù)結(jié)構(gòu)選擇:選擇更合理的數(shù)據(jù)結(jié)構(gòu)。例如,使用哈希表進(jìn)行快速查找,而不是數(shù)組。數(shù)組需要遍歷查找,而Hash表則可以根據(jù)下標(biāo)快速定位。
- 循環(huán)展開(kāi):將循環(huán)操作改為同樣的邏輯多次執(zhí)行。這個(gè)好處有很多,首先可以減少循環(huán)條件判斷和跳轉(zhuǎn)的處理,然后有利于提高CPU內(nèi)部的指令預(yù)測(cè)準(zhǔn)確度、指令并行度,以及提高CPU緩存的命中率。
- 延遲計(jì)算:只有在需要結(jié)果的時(shí)候才進(jìn)行計(jì)算,避免不必要的計(jì)算。很多函數(shù)式編程的方法中都大量使用了這一技術(shù),比如C#中針對(duì)列表的LINQ查詢(xún),只有在真的需要處理列表中某條數(shù)據(jù)的時(shí)候才執(zhí)行相應(yīng)的查詢(xún)算法,而不需要提前對(duì)列表中的所有數(shù)據(jù)進(jìn)行處理。在Web前端也有很多的延遲處理方案,比如圖片的懶加載,只在需要顯示圖片的時(shí)候才去加載,對(duì)于圖片比較多的頁(yè)面,可以大幅提升頁(yè)面的加載速度。
- 異步計(jì)算:程序只處理事務(wù)中的關(guān)鍵部分,然后就給調(diào)用方返回一個(gè)響應(yīng),邊緣事務(wù)或者慢速部分的處理通過(guò)發(fā)送消息的方式,由后臺(tái)其它程序慢慢處理。比如很多的秒殺、搶購(gòu)程序都采用這種方案,先把用戶(hù)的請(qǐng)求收下來(lái),只是發(fā)到一個(gè)待處理的隊(duì)列,然后就給用戶(hù)反饋已經(jīng)收到你的請(qǐng)求、正在處理中;同時(shí)后臺(tái)再有若干程序按照順序處理隊(duì)列中的消息,處理完畢后再給用戶(hù)反饋?zhàn)罱K的結(jié)果。
- 避免重復(fù)計(jì)算:通過(guò)緩存計(jì)算結(jié)果來(lái)避免重復(fù)執(zhí)行需要花費(fèi)大量時(shí)間的計(jì)算。
- 并行計(jì)算:利用多線(xiàn)程或多進(jìn)程來(lái)同時(shí)執(zhí)行任務(wù),特別是有多核CPU的時(shí)候。
在進(jìn)行優(yōu)化的時(shí)候,我們也要區(qū)分重點(diǎn),可以先通過(guò)代碼分析工具找到執(zhí)行最頻繁的部分,然后再進(jìn)行優(yōu)化。
1.1.3 使用更好的編譯器
好的編譯器,就像是一流的廚師,能用更少的步驟做出美味的菜。
上邊提到優(yōu)化代碼邏輯時(shí)可以采用“循環(huán)展開(kāi)”的方法,其實(shí)這件事完全可以交給編譯器去做,好的編譯器可以代替人工來(lái)完成這件事,程序員就有更多時(shí)間來(lái)思考業(yè)務(wù)邏輯。
除了“循環(huán)展開(kāi)”,編譯器還可以做“循環(huán)合并”,針對(duì)同一組數(shù)據(jù),如果代碼中編寫(xiě)了多次循環(huán)迭代,并且迭代的方法都是一樣的,編譯器可以將多次迭代合并成一次。
編譯器還可以做很多優(yōu)化,比如移除無(wú)用的代碼、內(nèi)聯(lián)函數(shù)減少壓棧、計(jì)算常量表達(dá)式的值、使用常量替換變量、使用更優(yōu)的算法和數(shù)據(jù)結(jié)構(gòu)、重排程序指令以利于CPU并行執(zhí)行,等等。
不過(guò)大多數(shù)情況下,比如使用Java、.NET時(shí),我們使用官方推薦或者IDE集成的編譯器就足夠了,只有在針對(duì)一些特定計(jì)算平臺(tái)或者特定的領(lǐng)域時(shí),我們才需要進(jìn)行選擇。
1.2 硬件層面的升級(jí)
在硬件層面要縮減程序的運(yùn)行時(shí)間,也就是更換運(yùn)行速度更快的硬件,比如使用主頻更高的CPU,1GHZ的CPU每條指令的執(zhí)行時(shí)間是1納秒,如果更換為3GHZ的CPU,每條指令的執(zhí)行時(shí)間可以降低到0.3納秒。
1.2.1 提升CPU性能
提升CPU就像是給餐廳請(qǐng)一個(gè)更快的廚師,他做菜的速度更快。
提升主頻:上邊我們已經(jīng)說(shuō)過(guò),提升主頻可以降低每條指令的執(zhí)行時(shí)間。但是主頻的提升不是無(wú)限的,因?yàn)橹黝l越高,電子元器件的散熱、穩(wěn)定性、成本等都會(huì)成為新的問(wèn)題,所以必須在這其中進(jìn)行平衡。
指令并行技術(shù):現(xiàn)代CPU中已經(jīng)產(chǎn)生了很多并行執(zhí)行指令的技術(shù),包括亂序執(zhí)行、分支預(yù)測(cè)技術(shù)、超標(biāo)量技術(shù)、多發(fā)射技術(shù)等,這些都像是廚師在同時(shí)處理多個(gè)菜品,而不是一個(gè)個(gè)來(lái),這自然能降低整體的響應(yīng)時(shí)間,當(dāng)然CPU肯定也要兼顧邏輯順序。
CPU緩存:現(xiàn)代CPU中很大一塊面積都是用來(lái)放置CPU緩存的,上文在【內(nèi)存的IO優(yōu)化】中我們提到過(guò)使用CPU緩存可以大幅降低CPU讀取指令的時(shí)間消耗,同時(shí)我們還需要注意到CPU存在多個(gè)核心時(shí),數(shù)據(jù)可能會(huì)被加載到不同的核心緩存中,數(shù)據(jù)在不同緩存中的同步也是一項(xiàng)很有挑戰(zhàn)的工作,因此足夠大的CPU緩存和足夠好的CPU緩存更新機(jī)制,對(duì)于降低響應(yīng)時(shí)間也很關(guān)鍵。
增加核心:主頻無(wú)法大幅提升時(shí),可以在CPU中多增加幾個(gè)核心,每個(gè)核心就是一個(gè)獨(dú)立的處理器,不同的線(xiàn)程、進(jìn)程可以并行運(yùn)行在不同的核心上,這樣也可以降低爭(zhēng)搶?zhuān)⑿卸仍礁?,程序的?zhí)行時(shí)間相對(duì)也越低。
我們?cè)谶x擇CPU時(shí),應(yīng)該結(jié)合業(yè)務(wù)特點(diǎn),綜合考慮以上這些方面。
1.2.2 跳過(guò)CPU的技術(shù)
理論上,計(jì)算機(jī)中所有的計(jì)算都是CPU來(lái)處理的,不過(guò)我們也可以讓它只處理最重要的事,一些不太重要的事就授權(quán)給其它部件處理,就像廚房中洗菜、切菜這些事都交給小工去處理,大廚專(zhuān)注于炒菜,可以讓出菜的速度更快。
在計(jì)算中有一種DMA技術(shù)就是干這個(gè)事的,比如使用支持DMA的網(wǎng)卡時(shí),網(wǎng)卡可以將數(shù)據(jù)直接寫(xiě)入到一塊內(nèi)存區(qū)域,等寫(xiě)滿(mǎn)了再通知CPU來(lái)讀取,而不是讓CPU一開(kāi)始就從網(wǎng)卡一點(diǎn)點(diǎn)讀取,這會(huì)大幅提高網(wǎng)絡(luò)數(shù)據(jù)處理的效率。因?yàn)榫W(wǎng)卡的速度相對(duì)CPU要慢的多,沒(méi)必要在這里耗著,等數(shù)據(jù)接收到內(nèi)存之后,CPU再和內(nèi)存打交道,速度就會(huì)快很多了。
對(duì)于需要集成IO設(shè)備進(jìn)行處理的程序,我們可以盡量選擇支持DMA技術(shù)的硬件。
1.2.3 使用專(zhuān)用硬件
這個(gè)就像做飯時(shí)使用不同的廚具,雖然我們也可以在湯鍋里炒菜,但是總不如炒鍋用的順手,用的順手就可以做到更快的速度。
在計(jì)算機(jī)中CPU是一種通用計(jì)算器,它可以進(jìn)行各種運(yùn)算,但是通用也有通用的壞處,那就是干一些事的時(shí)候效率不高,比如圖像處理、深度學(xué)習(xí)算法的運(yùn)算,這些運(yùn)算的特點(diǎn)就是包含大量的向量計(jì)算,CPU執(zhí)行向量運(yùn)算的效率比較低。
為了加速圖像處理,科技工作者們搞出了GPU,效率有了很大的提升,計(jì)算速度飛起。GPU一開(kāi)始是專(zhuān)門(mén)用于圖形計(jì)算的,圖形計(jì)算的主要工作就是向量運(yùn)算,而深度學(xué)習(xí)也主要是向量計(jì)算,所以GPU后來(lái)也被大量用于深度學(xué)習(xí)。再到后來(lái),科技工作者又搞出來(lái)了TPU,這種設(shè)備更加有利于深度學(xué)習(xí)的計(jì)算。
另外針對(duì)一些需要頻繁讀寫(xiě)硬盤(pán)的程序,比如數(shù)據(jù)庫(kù)程序,我們也推薦使用固態(tài)硬盤(pán)代替機(jī)械硬盤(pán),因?yàn)楣虘B(tài)硬盤(pán)的訪(fǎng)問(wèn)延遲相比機(jī)械硬盤(pán)會(huì)低1個(gè)或多個(gè)數(shù)量級(jí),這會(huì)大幅降低數(shù)據(jù)讀寫(xiě)的延遲。
所以針對(duì)不同的計(jì)算特點(diǎn),我們可以選擇更專(zhuān)用的硬件來(lái)加速程序的處理,這是個(gè)不錯(cuò)的方案。
---
當(dāng)然使用性能更高的硬件,需要付出更多的成本,需要仔細(xì)評(píng)估。以前技術(shù)開(kāi)發(fā)領(lǐng)域流傳過(guò)有一句話(huà):不要對(duì)程序做過(guò)多的優(yōu)化,升級(jí)下硬件就行了。這是因?yàn)楫?dāng)時(shí)升級(jí)硬件的成本要遠(yuǎn)低于優(yōu)化程序的時(shí)間成本,不過(guò)隨著互聯(lián)網(wǎng)的發(fā)展,人們對(duì)性能的追求越來(lái)越高,升級(jí)硬件的難度和成本也越來(lái)越高,這句話(huà)變得不是那么可靠。從Go、Rust等語(yǔ)言的流行,Java、.NET等對(duì)原生編譯的支持,我們也可以感受到這個(gè)趨勢(shì),硬件資源開(kāi)始變得稀缺了。
吞吐量
吞吐量就像餐廳一天能服務(wù)多少客人。
在計(jì)算機(jī)里,它指的是單位時(shí)間內(nèi)處理的請(qǐng)求數(shù)、數(shù)據(jù)量或執(zhí)行的指令數(shù)。
2.1 縮短響應(yīng)時(shí)間
縮短響應(yīng)時(shí)間自然能提高吞吐量,就像提高廚師做菜速度能讓更多客人吃到菜一樣。
上文已經(jīng)介紹了響應(yīng)時(shí)間的很多優(yōu)化方法,這里及不重復(fù)了。
但是響應(yīng)時(shí)間對(duì)吞吐量的影響不一定總是正面的。降低響應(yīng)時(shí)間有可能會(huì)增加資源的使用,比如我們把原本放在硬盤(pán)中的數(shù)據(jù)都放到了內(nèi)存中,然后就沒(méi)有足夠的內(nèi)存用來(lái)創(chuàng)建新的線(xiàn)程,服務(wù)器就無(wú)法接收更多的請(qǐng)求,吞吐量也就無(wú)法提升。
2.2 增加并發(fā)能力
2.2.3 增加資源
這是最直接的方法,就像買(mǎi)更多的爐子和鍋,就能同時(shí)做更多的菜。
在計(jì)算機(jī)領(lǐng)域,我們可以購(gòu)買(mǎi)更多的服務(wù)器、更強(qiáng)勁的CPU或GPU、更大的內(nèi)存、讀寫(xiě)能力更強(qiáng)的IO設(shè)備、更大的網(wǎng)絡(luò)帶寬,等等。這些可以讓程序在單位時(shí)間內(nèi)接收更多的請(qǐng)求、以及更大的并發(fā)處理能力,也就增加了系統(tǒng)的吞吐量。當(dāng)然在具體的增加某種資源之前,我們需要先找到系統(tǒng)的瓶頸,比如內(nèi)存使用經(jīng)常達(dá)到90%,我們就增加內(nèi)存空間。
需要注意,增加資源雖然可以提升系統(tǒng)的吞吐量,但是這也會(huì)有一個(gè)臨界點(diǎn),越過(guò)這個(gè)臨界點(diǎn)之后,獲得的收益將會(huì)低于為此增加的資源成本,所以我們應(yīng)該仔細(xì)評(píng)估收益和成本,再?zèng)Q定增加多少資源。
同時(shí)本著節(jié)約的精神,我們應(yīng)該追求使用更少的資源來(lái)完成更多的工作,這樣也能產(chǎn)生更多的收益。
2.2.2 利用CPU的先進(jìn)技術(shù)
上文我們?cè)凇?.2.1 提升CPU性能】中已經(jīng)提到過(guò)CPU的指令并行技術(shù),包括流水線(xiàn)、分支預(yù)測(cè)、亂序執(zhí)行、多發(fā)射、超標(biāo)量等,它們可以讓CPU同時(shí)執(zhí)行多條指令,提升CPU的指令吞吐量,自然也就提升了程序的處理吞吐量。
這些都是CPU內(nèi)部的技術(shù)優(yōu)化,只要購(gòu)買(mǎi)性能優(yōu)良的CPU,我們就可以擁有這些能力;同時(shí)我們?cè)诰幊虝r(shí)也可以盡量觸發(fā)指令的并行執(zhí)行,比如上文【1.1.2 優(yōu)化代碼邏輯】中提到的循環(huán)展開(kāi)、內(nèi)聯(lián)函數(shù)等,當(dāng)然我更推薦選擇一個(gè)更好的編譯器來(lái)完成這些優(yōu)化工作,程序員應(yīng)該多思考下怎么運(yùn)用技術(shù)滿(mǎn)足業(yè)務(wù)需求。
除了CPU微觀層面的優(yōu)化,我們還可以利用下CPU的多核并行能力,在程序中使用多線(xiàn)程、多進(jìn)程,讓程序并行處理,從而提升程序的業(yè)務(wù)吞吐量。
CPU的這些能力就像是多個(gè)廚師協(xié)作,同時(shí)準(zhǔn)備不同的菜品,就能在單位時(shí)間內(nèi)把更多的菜品端上桌。
2.2.1 提高資源利用率
大家可能聽(tīng)說(shuō)過(guò)Go的并發(fā)能力特別強(qiáng),簡(jiǎn)單手?jǐn)]一個(gè)服務(wù)就能輕松應(yīng)對(duì)百萬(wàn)并發(fā),原因就是因?yàn)镚o搞出了協(xié)程。那么協(xié)程為什么這么優(yōu)秀呢?
這是因?yàn)槌绦蚴褂脗鹘y(tǒng)的線(xiàn)程模型時(shí),線(xiàn)程消耗的時(shí)間和空間成本比較高,時(shí)間成本就是CPU的使用時(shí)間,空間成本就是內(nèi)存占用,要提升吞吐量只能增加更多的CPU和內(nèi)存資源,資源利用率高不起來(lái),具體是怎么回事呢?
首先看我們編寫(xiě)的業(yè)務(wù)程序,其實(shí)大部分工作都是很多IO操作,比如訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)、請(qǐng)求網(wǎng)絡(luò)接口、讀寫(xiě)文件等,這些IO操作相比CPU指令操作慢了4、5個(gè)數(shù)量級(jí)。使用線(xiàn)程模型時(shí),發(fā)起IO請(qǐng)求后,當(dāng)前線(xiàn)程使用的CPU要么等著要么切換給其它線(xiàn)程使用,等著CPU就是空轉(zhuǎn),切換給其它線(xiàn)程時(shí)的成本也比較高,總之就是浪費(fèi)了CPU時(shí)間;另外在等待IO返回的這段時(shí)間內(nèi),線(xiàn)程不會(huì)消失,會(huì)一直存在,而線(xiàn)程占用的內(nèi)存比較大(Windows默認(rèn)1M,Linux默認(rèn)8M),妥妥的站著什么不拉什么。這就是線(xiàn)程的時(shí)間成本和空間成本問(wèn)題。
解決這個(gè)問(wèn)題的第一步是使用異步編程:IO操作提交后,就把線(xiàn)程釋放掉,程序也不用在這里等著,等IO返回結(jié)果時(shí),操作系統(tǒng)再分配一個(gè)新的線(xiàn)程進(jìn)行處理。線(xiàn)程少了,CPU等待、切換和內(nèi)存占用也就少了,計(jì)算資源自然就可以支持更多的請(qǐng)求了,吞吐量也就上來(lái)了。這就像是廚師在等待一個(gè)菜烤制的同時(shí),可以去炒另一道菜。
協(xié)程則是在異步的基礎(chǔ)上更進(jìn)一步,把程序執(zhí)行的最小單位由線(xiàn)程變成了協(xié)程,協(xié)程分配的內(nèi)存更小,初始時(shí)僅為2KB,不過(guò)它可以隨著任務(wù)執(zhí)行按需增長(zhǎng),最大可達(dá)1GB。同樣的8M內(nèi)存,Linux中只能創(chuàng)建1個(gè)線(xiàn)程,而協(xié)程最多則可以創(chuàng)建4096個(gè)。我們也就能夠理解Go的并發(fā)能力為什么這么強(qiáng)了。
總結(jié)
性能優(yōu)化是一個(gè)復(fù)雜且多面的話(huà)題,涉及到代碼的編寫(xiě)、系統(tǒng)的架構(gòu)以及硬件的選擇與配置。在追求性能的旅途中,我們需要掌握的知識(shí)有很多,既有軟件方面的,也有硬件方面的,很多東西我也沒(méi)有展開(kāi)詳細(xì)講,只是給大家提供了一個(gè)引子,遇到問(wèn)題的時(shí)候可以順著它去尋找。
在優(yōu)化過(guò)程中,還需要注意性能的提升并非總是線(xiàn)性的,我們應(yīng)當(dāng)找到系統(tǒng)的瓶頸點(diǎn),有針對(duì)性地優(yōu)化,并在資源成本和性能收益之間做出平衡。優(yōu)化的最終目的是在有限的資源下,盡可能地提升程序的響應(yīng)速度和處理能力。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-776866.html
關(guān)注螢火架構(gòu),加速技術(shù)提升!文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-776866.html
到了這里,關(guān)于8000字,程序性能優(yōu)化的全能手冊(cè)的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!