軟件教練說:性能優(yōu)化與性能設(shè)計(jì),一對(duì)“相親相愛”的

      網(wǎng)友投稿 761 2025-03-31

      性能優(yōu)化是指在不影響正確性的前提下,使程序運(yùn)行得更快,它是一個(gè)非常廣泛的話題。

      優(yōu)化有時(shí)候是為了降低成本,但有時(shí)候,性能能決定一個(gè)產(chǎn)品的成敗,比如游戲服務(wù)器的團(tuán)戰(zhàn)玩法需要單服達(dá)到一定的同時(shí)在線人數(shù)才能支撐起這類玩法,而電信軟件的性能往往是競(jìng)標(biāo)的核心競(jìng)爭(zhēng)力,性能關(guān)乎商業(yè)成敗。

      軟件產(chǎn)品多種多樣,影響程序執(zhí)行效率的因素很多,因此,性能優(yōu)化,特別是對(duì)不熟悉的項(xiàng)目做優(yōu)化,不是一件容易的事。

      性能優(yōu)化可分為宏觀和微觀兩個(gè)層面。宏觀層面包括架構(gòu)重構(gòu),而微觀層面,則包括算法的優(yōu)化,編譯優(yōu)化,工具分析,高性能編碼等,這些方法是有可能獨(dú)立于具體業(yè)務(wù)邏輯,因而有更加廣泛的適應(yīng)性,且更易于實(shí)施。

      具體到性能優(yōu)化的方法論,首先,應(yīng)建立度量,你度量什么,你得到什么。所以,性能優(yōu)化測(cè)試先行,須基于數(shù)據(jù)而不能憑空猜測(cè),這是做優(yōu)化的一個(gè)基本原則。搭建真實(shí)的壓測(cè)環(huán)境,或者逼近真實(shí)環(huán)境,有時(shí)候是困難的,也可能非常耗費(fèi)時(shí)間,但它依然是值得的。

      有許多工具能幫助我們定位程序瓶頸,有些工具能做很友好的圖形化展示,定位問題是解決問題的前置條件,但定位問題可能不是最難的,分析和優(yōu)化才是最耗時(shí)的關(guān)鍵環(huán)節(jié),修改之后,要再回歸測(cè)試,驗(yàn)證是否如預(yù)期般有效。

      什么是高性能程序?架構(gòu)致廣遠(yuǎn)、實(shí)現(xiàn)盡精微。

      架構(gòu)優(yōu)化的關(guān)鍵是識(shí)別瓶頸,這類優(yōu)化有很多套路:比如通過負(fù)載均衡做分布式改造,比如用多線程協(xié)程做并行化改造,比如用消息隊(duì)列做異步化和解耦,比如用事件通知替代輪詢,比如為數(shù)據(jù)訪問增加緩存,比如用批處理+預(yù)取提升吞吐,比如IO與邏輯分離、讀寫分離等等。

      架構(gòu)調(diào)整和優(yōu)化雖然收效很大,卻因受限于各種現(xiàn)實(shí)因素,因而并不總是可行。

      能不做的盡量不做、必須做的高效做是性能優(yōu)化的一個(gè)根本法則,提升處理能力和降低計(jì)算量可視為性能優(yōu)化的兩個(gè)方向。

      有時(shí)候,我們不得不從細(xì)節(jié)的維度去改進(jìn)程序。通常,我們應(yīng)該使用簡(jiǎn)單的數(shù)據(jù)結(jié)構(gòu)和算法,但如有必要,就應(yīng)積極使用更高效的結(jié)構(gòu)和算法,不止邏輯結(jié)構(gòu),實(shí)現(xiàn)結(jié)構(gòu)(存儲(chǔ))同樣影響執(zhí)行效率;分支預(yù)測(cè)、反饋優(yōu)化、啟發(fā)性以及基于機(jī)器學(xué)習(xí)編譯優(yōu)化的效果日益凸顯;熟練掌握編程語言深刻理解標(biāo)準(zhǔn)庫實(shí)現(xiàn)能幫助我們規(guī)避低性能陷阱;深入細(xì)節(jié)做代碼微調(diào)甚至指令級(jí)優(yōu)化有時(shí)候也能取得意想不到的效果。

      有時(shí)候,我們需要做一些交換,比如用空間置換時(shí)間,比如犧牲一些通用性可讀性換取高性能,我們只應(yīng)當(dāng)在非常必要的情況下才這么做,它是權(quán)衡的藝術(shù)。

      1、架構(gòu)優(yōu)化

      通常系統(tǒng)的throughput越大,latency就越高,但過高的latency不可接受,所以架構(gòu)優(yōu)化不是一味追求throughput,也需要關(guān)注latency,追求可接受latency下的高throughput。

      負(fù)載均衡

      負(fù)載均衡其實(shí)就是解決一個(gè)分活的問題,對(duì)應(yīng)到分布式系統(tǒng),一般在邏輯服的前面都會(huì)安放一個(gè)負(fù)載均衡器,比如NGINX就是經(jīng)典的解決方案。負(fù)載均衡不限于分布式系統(tǒng),對(duì)于多線程架構(gòu)的服務(wù)器內(nèi)部,也需要解決負(fù)載均衡的問題,讓各個(gè)worker線程的負(fù)載均衡。

      多線程、協(xié)程并行化

      雖然硬件架構(gòu)的復(fù)雜化對(duì)程序開發(fā)提出了更高的要求,但編寫充分利用多CPU多核特性的程序能獲得令人驚嘆的收益,所以,在同樣硬件規(guī)格下,基于多線程/協(xié)程的并行化改造依然值得嘗試。

      多線程不可避免要面臨資源競(jìng)爭(zhēng)的問題,我們的設(shè)計(jì)目標(biāo)應(yīng)該是充分利用硬件多執(zhí)行核心的優(yōu)勢(shì),減少等待,讓多個(gè)執(zhí)行流暢快的奔跑起來。

      對(duì)于多線程模型,如果把每一個(gè)要干的活抽象為一個(gè)task,把干活的線程抽象為worker,那么,有兩種典型的設(shè)計(jì)思路,一種是對(duì)task類型做出劃分,讓一類或者一個(gè)worker去干特定的task,另一種是讓所有worker去干所有task。

      第一種劃分,能減少數(shù)據(jù)爭(zhēng)用,編碼實(shí)現(xiàn)也更簡(jiǎn)單,只需要識(shí)別有限的競(jìng)爭(zhēng),就能讓系統(tǒng)工作的很好,缺點(diǎn)是任務(wù)的工作量很可能不同,有可能導(dǎo)致有些worker忙碌而另一些空閑。

      第二種劃分,優(yōu)點(diǎn)是能均衡,缺點(diǎn)是編碼復(fù)雜性高,數(shù)據(jù)競(jìng)爭(zhēng)多。

      有時(shí)候,我們會(huì)綜合上述兩種模式,比如讓單獨(dú)的線程去做IO(收發(fā)包)+反序列化(產(chǎn)生protocol task),然后啟動(dòng)一批worker線程去處理包,中間通過一個(gè)task queue去連接,這即是經(jīng)典的生產(chǎn)者消費(fèi)者模型。

      協(xié)程是一種用戶態(tài)的多執(zhí)行流,它基于一個(gè)假設(shè),即用戶態(tài)的任務(wù)切換成本低于系統(tǒng)的線程切換。

      通知替代輪詢

      輪詢即不停詢問,就像你每隔幾分鐘去一趟宿管那里查看是否有信件,而通知是你告訴宿管阿姨,你有信的時(shí)候,她打電話通知你,顯然輪詢耗費(fèi)CPU,而通知機(jī)制效率更高。

      添加緩存

      緩存的理論依據(jù)是局部性原理。

      一般系統(tǒng)的寫入請(qǐng)求遠(yuǎn)少于讀請(qǐng)求,針對(duì)寫少讀多的場(chǎng)景,很適合引入緩存集群。

      在寫數(shù)據(jù)庫的時(shí)候同時(shí)寫一份數(shù)據(jù)到緩存集群里,然后用緩存集群來承載大部分的讀請(qǐng)求,因?yàn)榫彺婕汉苋菀鬃龅礁咝阅埽裕@樣的話,通過緩存集群,就可以用更少的機(jī)器資源承載更高的并發(fā)。

      緩存的命中率一般能做到很高,而且速度很快,處理能力也強(qiáng)(單機(jī)很容易做到幾萬并發(fā)),是理想的解決方案。

      CDN本質(zhì)上就是緩存,被用戶大量訪問的靜態(tài)資源緩存在CDN中是目前的通用做法。

      消息隊(duì)列

      消息隊(duì)列、消息中間件是用來做寫請(qǐng)求異步化,我們把數(shù)據(jù)寫入MessageQueue就認(rèn)為寫入完成,由MQ去緩慢的寫入DB,它能起到削峰填谷的效果。

      消息隊(duì)列也是解耦的手段,它主要用來解決寫的壓力。

      IO與邏輯分離、讀寫分離

      IO與邏輯分離,這個(gè)前面已經(jīng)講了。讀寫分離是一種數(shù)據(jù)庫應(yīng)對(duì)壓力的慣用措施,當(dāng)然,它也不僅限于DB。

      批處理與數(shù)據(jù)預(yù)取

      批處理是一種思想,分很多種應(yīng)用,比如多網(wǎng)絡(luò)包的批處理,是指把收到的包攢到一起,然后一起過一遍流程,這樣,一個(gè)函數(shù)被多次調(diào)用,或者一段代碼重復(fù)執(zhí)行多遍,這樣i-cache的局部性就很好,另外,如果這個(gè)函數(shù)或者一段里要訪問的數(shù)據(jù)被多次訪問,d-cache的局部性也能改善,自然能提升性能,批處理能增加吞吐,但通常會(huì)增大延遲。

      另一個(gè)批處理思想的應(yīng)用是日志落盤,比如一條日志大概寫幾十個(gè)字節(jié),我們可以把它緩存起來,攢夠了一次寫到磁盤,這樣性能會(huì)更好,但這也帶來數(shù)據(jù)丟失的風(fēng)險(xiǎn),不過通常我們可以通過shm的方式規(guī)避這個(gè)風(fēng)險(xiǎn)。

      指令預(yù)取是CPU自動(dòng)完成的,數(shù)據(jù)預(yù)取是一個(gè)很有技巧性的工作,數(shù)據(jù)預(yù)取的依據(jù)是預(yù)取的數(shù)據(jù)將在接下來的操作中用到,它符合空間局部性原理,數(shù)據(jù)預(yù)取可以填充流水線,降低訪存等待,但數(shù)據(jù)預(yù)取會(huì)侵害代碼,且并不總?cè)珙A(yù)期般有效。

      哪怕你不增加預(yù)取代碼,硬件預(yù)取器也有可能幫你做預(yù)取,另外gcc也有編譯選項(xiàng),開啟它會(huì)在編譯階段自動(dòng)插入預(yù)取代碼,手動(dòng)增加預(yù)取代碼需要小心處理,時(shí)機(jī)的選擇很重要,最后一定要基于測(cè)試數(shù)據(jù),另外,即使預(yù)取表現(xiàn)很好,但代碼修改也有可能導(dǎo)致效果衰減,而且預(yù)取語句執(zhí)行本身也有開銷,只有預(yù)取的收益大于預(yù)取的開銷,且CACHE-MISS很高才是值得的。

      2、算法優(yōu)化

      數(shù)據(jù)量小的集合上遍歷查找即可,但如果循環(huán)的次數(shù)過百,便需要考慮用更快的查找結(jié)構(gòu)和算法替換蠻力遍歷,哈希表,紅黑樹,二分查找很常用。

      哈希(HASH)

      哈希也叫散列,是把任意長(zhǎng)度的輸入通過散列算法變換成固定長(zhǎng)度的輸出,該輸出就是散列值,也叫摘要。比如把一篇文章的內(nèi)容通過散列生成64位的摘要,該過程不可逆。

      這種轉(zhuǎn)換是一種壓縮映射,也就是,散列值的空間通常遠(yuǎn)小于輸入的空間,不同的輸入可能會(huì)散列成相同的輸出,所以不可能從散列值來確定唯一的輸入值,但如果輸出的位數(shù)足夠,散列成相同輸出的概率非常非常小。

      字符串的比較有時(shí)會(huì)成為消耗較大的操作,雖然strcmp或者memcpy的實(shí)現(xiàn)用到了很多加速和優(yōu)化技巧,但本質(zhì)上它還是逐個(gè)比較的方式。

      字符串比較的一個(gè)改進(jìn)方案就是哈希,比較哈希值(通常是一個(gè)int64的整數(shù))而非比較內(nèi)容能快很多,但需要為字符串提前計(jì)算好哈希值,且需要額外的空間保存哈希值,另外,在哈希值相等的時(shí)候,還需要比較字符串,但因?yàn)闆_突的概率極低,所以后續(xù)的字符串比較不會(huì)很多次。

      這樣不一定總是更高效,但它提供了另一個(gè)思路,你需要測(cè)試你的程序,再?zèng)Q定要不要這樣做。

      另一個(gè)哈希的用法是哈希表,哈希表的經(jīng)典實(shí)現(xiàn)是提前開辟一些桶,通過哈希找到元素所在的桶(編號(hào)),如果沖突,再拉鏈解決沖突。

      為了減少?zèng)_突經(jīng)常需要開辟更多的桶,但更多的桶需要更大的存儲(chǔ)空間,特別是元素?cái)?shù)量不確定的時(shí)候,桶的數(shù)量選擇變得兩難,隨著裝載的元素變多,沖突加劇,在擴(kuò)容的時(shí)候,將需要對(duì)已存在的元素重新哈希,這是很費(fèi)的點(diǎn)。

      哈希表的沖突極端情況下會(huì)退化成鏈表,當(dāng)初設(shè)想的快速查找變得不再可行,HashMap是普通哈希表的改進(jìn)版,結(jié)合哈希和二叉平衡搜索樹。

      另一個(gè)常用來做查找的結(jié)構(gòu)是紅黑樹,它能確保最壞情況下,有l(wèi)ogN的時(shí)間復(fù)雜度,但紅黑樹的查找過程需要沿著鏈走,不同結(jié)點(diǎn)內(nèi)存通常不連續(xù),CACHE命中性經(jīng)常很差,紅黑樹的中序遍歷結(jié)果是有序的,這是哈希表不具備的,另外,紅黑樹不存在哈希表那般預(yù)估容量難的問題。

      基于有序數(shù)組的二分查找

      二分查找的時(shí)間復(fù)雜度也是logN,跟紅黑樹一致,但二分查找的空間局部性更好,不過二分查找有約束,它只能在有序數(shù)組上進(jìn)行,所以,如果你需要在固定的數(shù)據(jù)集合(比如配置數(shù)據(jù))做查找,二分查找是個(gè)不錯(cuò)的選擇。

      跳表(Skip List)

      跳表增加了向前指針,是一種多層結(jié)構(gòu)的有序鏈表,插入一個(gè)值時(shí)有一定概率晉升到上層形成間接的索引。

      跳表是一個(gè)隨機(jī)化的數(shù)據(jù)結(jié)構(gòu),實(shí)質(zhì)就是一種可以進(jìn)行二分查找的有序鏈表。跳表在原有的有序鏈表上面增加了多級(jí)索引,通過索引來實(shí)現(xiàn)快速查找。跳表不僅能提高搜索性能,同時(shí)也可以提高插入和刪除操作的性能。

      跳表適合大量并發(fā)寫的場(chǎng)景,可以認(rèn)為是隨機(jī)平衡的二叉搜索樹,不存在紅黑樹的再平衡問題。Redis強(qiáng)大的ZSet底層數(shù)據(jù)結(jié)構(gòu)就是哈希加跳表。

      相比哈希表和紅黑樹,跳表用的不那么多。

      數(shù)據(jù)結(jié)構(gòu)的實(shí)現(xiàn)優(yōu)化

      我們通常只會(huì)講數(shù)據(jù)的邏輯結(jié)構(gòu),但數(shù)據(jù)的實(shí)現(xiàn)(存儲(chǔ))結(jié)構(gòu)也會(huì)影響性能。

      數(shù)組在存儲(chǔ)上一定是邏輯地址連續(xù)的,但鏈表不具有這樣的特點(diǎn),鏈表通過鏈域?qū)ふ遗R近節(jié)點(diǎn),如果相鄰節(jié)點(diǎn)在地址上發(fā)散,則沿著鏈域訪問效率不高,所以實(shí)現(xiàn)上可以通過從單獨(dú)的內(nèi)存配置器分配結(jié)點(diǎn)(盡量?jī)?nèi)存收斂)來優(yōu)化訪問效率,同樣的方法也適應(yīng)紅黑樹、哈希表等其他結(jié)構(gòu)。

      排序

      盡量對(duì)指針、索引、ID排序,而不要對(duì)對(duì)象本身排序,因?yàn)榻粨Q對(duì)象比交換地址/索引慢;求topN不要做全排序;非穩(wěn)定排序能滿足要求不要搞穩(wěn)定排序。

      延遲計(jì)算 &? 寫時(shí)拷貝

      延遲計(jì)算和寫時(shí)拷貝(COW)思想上是一樣的,即可以通過把計(jì)算盡量推遲來減少計(jì)算開銷。

      我拿游戲服務(wù)器開發(fā)來舉例,假設(shè)玩家的戰(zhàn)斗力(fight)是通過等級(jí),血量,稱號(hào)等其他屬性計(jì)算出來的,我們可以在等級(jí)、血量、稱號(hào)變化的時(shí)候立即重算fight,但血量可能變化比較頻繁,所以就會(huì)需要頻繁重算戰(zhàn)力。通過延遲計(jì)算,我們可以為戰(zhàn)力添加一個(gè)dirtyFlag,在等級(jí)、血量、稱號(hào)變化的時(shí)候設(shè)置dirtyFlag,等到真正需要用到戰(zhàn)力的時(shí)候(GetFight函數(shù))里判斷dirtyFlag,如果dirtyFlag為true則重算戰(zhàn)力并清dirtyFlag,如果dirtyFlag為false則直接返回fight值。

      寫時(shí)拷貝(COW)跟這個(gè)差不多,linux kernel在fork進(jìn)程的時(shí)候,子進(jìn)程會(huì)共享父進(jìn)程的地址空間,只有在子進(jìn)程對(duì)自身地址空間寫的時(shí)候,才會(huì)clone一份出來,同樣,string的設(shè)計(jì)也用到了類似的思想。

      預(yù)計(jì)算

      有些值可以提前計(jì)算出結(jié)果并保存起來,不用重復(fù)計(jì)算的盡量不重復(fù)計(jì)算,特別是循環(huán)內(nèi)的計(jì)算,要避免重復(fù)的常量計(jì)算,C++甚至增加了一個(gè)constexpr的關(guān)鍵詞。

      增量更新

      增量更新的原理不復(fù)雜,只做增量,只做DIFF,不做全量,這個(gè)思想有很多應(yīng)用場(chǎng)景。

      舉個(gè)例子,游戲服務(wù)器每隔一段時(shí)間需要把玩家的屬性(比如血量、魔法值等)同步到客戶端,簡(jiǎn)單的做法是把所有屬性打包一次性全發(fā)送過去,這樣比較耗費(fèi)帶寬,可以考慮為每個(gè)屬性編號(hào),在發(fā)送的時(shí)候,只發(fā)送變化的屬性。

      在發(fā)送端,編碼一個(gè)變化的屬性的時(shí)候,需要發(fā)送一個(gè)屬性編號(hào)+屬性值的對(duì)子,接收端類似,先解出屬性編號(hào),再解出屬性值,這種方式可能需要犧牲一點(diǎn)CPU換帶寬。

      3、代碼優(yōu)化

      內(nèi)存優(yōu)化

      (a)小對(duì)象分配器

      C的動(dòng)態(tài)內(nèi)存分配是介于系統(tǒng)和應(yīng)用程序的中間層,malloc/free本身體現(xiàn)的就是一種按需分配+復(fù)用的思想。

      當(dāng)你調(diào)用malloc向glibc的動(dòng)態(tài)內(nèi)存分配器ptmalloc申請(qǐng)6字節(jié)的內(nèi)存,實(shí)際耗費(fèi)的會(huì)大于6字節(jié),6是動(dòng)態(tài)分配塊的有效載荷,動(dòng)態(tài)內(nèi)存分配器會(huì)為chunk添加首部和尾部,有時(shí)候還會(huì)加一下填充,所以,真正耗費(fèi)的存儲(chǔ)空間會(huì)遠(yuǎn)大于6字節(jié),在我的機(jī)器上,通過malloc_usable_size發(fā)現(xiàn)申請(qǐng)6字節(jié),返回的chunk,實(shí)際可用的size為24,加上首尾部就更多了。

      但你真正申請(qǐng)(可用)的大小是6字節(jié),可見,動(dòng)態(tài)內(nèi)存分配的chunk內(nèi)有大量的碎片,這就是內(nèi)碎片,而外碎片是存在chunk之間的,是另一個(gè)問題。

      當(dāng)你申請(qǐng)的size較大,有效載荷 /?耗費(fèi)空間的比例會(huì)比較高,內(nèi)碎片占比不高,但但size較小,這個(gè)占比就高,如果這種小size的chunk非常多,就會(huì)造成內(nèi)存的極大浪費(fèi)。

      《C++設(shè)計(jì)新思維》一書中的loki庫實(shí)現(xiàn)了一個(gè)小對(duì)象分配器,通過隱式鏈表的方式解決了這個(gè)問題,有興趣的可以去看看。

      (b)cached obj

      《C++ Primer》實(shí)現(xiàn)了一個(gè)CachedObj類模板,任何需要擁有這種cached能力的類型都可以通過從CachedObj派生而獲得。

      它的主要思想是為該種類型維護(hù)一個(gè)FreeList,每個(gè)節(jié)點(diǎn)就是一個(gè)Object,對(duì)象申請(qǐng)的時(shí)候,檢查FreeList,如果FreeList不為空,則摘除頭結(jié)點(diǎn)返回,如果FreeList為空,則new一批Object并串到FreeList,然后走FreeList不為空的分配流程,通過重載類的operator new和operator delete,達(dá)到對(duì)類的使用者透明的目的。

      (c)內(nèi)存分配和對(duì)象構(gòu)建分離

      c的malloc用來動(dòng)態(tài)分配內(nèi)存,free用來歸還內(nèi)存;C++的new做了3件事,通過operator new(本質(zhì)上等同malloc)分配內(nèi)存,在分配的內(nèi)存上構(gòu)建對(duì)象,返回對(duì)象指針;而delete干了兩件事,調(diào)用析構(gòu)函數(shù),歸還內(nèi)存。

      C++通過placement new可以分離內(nèi)存分配和對(duì)象構(gòu)建,結(jié)合顯示的析構(gòu)函數(shù)調(diào)用,達(dá)到自控的目的。

      我優(yōu)化過一個(gè)游戲項(xiàng)目,啟動(dòng)時(shí)間過長(zhǎng),記憶中需要幾十秒(至少十幾秒),分析后發(fā)現(xiàn)主要是因?yàn)橛螒驁?zhí)行預(yù)分配策略(對(duì)象池),在啟動(dòng)的時(shí)候按最大容量創(chuàng)建怪和玩家,對(duì)象構(gòu)建很重,大量對(duì)象構(gòu)建耗時(shí)過長(zhǎng),通過分離內(nèi)存分配和對(duì)象構(gòu)建,把對(duì)象構(gòu)建推遲到真正需要的時(shí)候,實(shí)現(xiàn)了服務(wù)的重啟秒起。

      (d)內(nèi)存復(fù)用

      編解碼、加解密、序列化反序列化(marshal/unmarshal)的時(shí)候一般都需要?jiǎng)討B(tài)申請(qǐng)內(nèi)存,這種調(diào)用頻次很高,可以考慮用靜態(tài)內(nèi)存,為了避免多線程競(jìng)爭(zhēng),可以用thread local。

      當(dāng)然你也可以改進(jìn)靜態(tài)內(nèi)存策略,比如封裝一個(gè)GetEncodeMemeory(size_t)函數(shù),維護(hù)一個(gè)void* + size_t結(jié)構(gòu)體對(duì)象(初始化為NULL+0),對(duì)比參數(shù)size跟對(duì)象的size成員,如果參數(shù)size<=對(duì)象size,直接返回對(duì)象大的void*指針,否則free掉void*指針,再按參數(shù)size分配一個(gè)更大的void*,并用參數(shù)size更新對(duì)象size。

      cache優(yōu)化

      i-cache優(yōu)化:i-cache的優(yōu)化可以通過精簡(jiǎn)code path,簡(jiǎn)化調(diào)用關(guān)系,減少代碼量,減少復(fù)雜度來實(shí)現(xiàn)。

      具體措施包括,減少函數(shù)調(diào)用(就地展開、inline),利用分支預(yù)測(cè),減少函數(shù)指針,可以考慮把code path上相關(guān)的函數(shù)定義在一起,把相關(guān)的函數(shù)定義到一個(gè)源文件,并讓它們?cè)谠次募吓R近,這樣生成的object文件,在運(yùn)行時(shí)加載后相關(guān)函數(shù)大概率也內(nèi)存臨近,當(dāng)然編譯器也一直在做這方面的努力,但我們寫代碼不應(yīng)該依賴編譯器優(yōu)化,盡量去幫助編譯器生成更高效的代碼。

      d-cache優(yōu)化:d-cache優(yōu)化包括改進(jìn)數(shù)據(jù)結(jié)構(gòu)和算法獲取更好的數(shù)據(jù)訪問時(shí)空局部性,比如二分查找就是d-cache友好算法。一個(gè)cache line一般是64B,如果數(shù)據(jù)跨越兩個(gè)cache-line,則會(huì)導(dǎo)致load & store2次,所以,需要結(jié)合cache對(duì)齊,盡量讓相關(guān)訪問的數(shù)據(jù)在一個(gè)cache-line。

      如果結(jié)構(gòu)體過大,則各成員不僅可能在不同cache-line,甚至可能在不同page,所以應(yīng)該避免結(jié)構(gòu)體過大。

      如果結(jié)構(gòu)體的成員變量過多,一般而言對(duì)各成員的訪問頻次也會(huì)滿足2-8定律,可以考慮把hot和cold的成員分開,重排結(jié)構(gòu)體成員變量順序,但這些騷操作我不建議在開始的時(shí)候用,因?yàn)檎f不定哪天又要增刪成員,從而破壞苦心孤詣搭建的積木。

      判斷前置

      判斷前置指在函數(shù)中講判斷返回的語句前置,這樣不至于忙活半天,你跟我說對(duì)不起不合適,玩兒呢?

      在寫多個(gè)判斷的時(shí)候,把不滿足可能性高的放在前面。

      軟件教練說:性能優(yōu)化與性能設(shè)計(jì),一對(duì)“相親相愛”的

      在寫條件或的時(shí)候把為true的放在前面,在寫條件與的時(shí)候把為false的放在前面。

      另外,如果在循環(huán)里調(diào)用一個(gè)函數(shù),而這個(gè)函數(shù)里檢查某條件,不符合就返回,這種情況,可以考慮把檢查放到調(diào)用函數(shù)的外面,這樣不滿足的話就不用陷入函數(shù),當(dāng)然,你也可以說,這樣的操作違背軟件工程,但看你想要什么,你不總是能夠兩全其美,對(duì)吧?

      湊零為整與化整為零

      湊零為整其實(shí)的思想在日志批處理里提了,不再展開。

      化整為零體現(xiàn)了分而治之的思想,可以把一個(gè)大的操作,分?jǐn)傞_來,避免在做大操作的時(shí)候?qū)е驴D,從而讓CPU占比更加平穩(wěn)。

      分頻

      之前我優(yōu)化過一個(gè)游戲服務(wù)器,游戲服務(wù)器的邏輯線程是一個(gè)大循環(huán),里面調(diào)用tick函數(shù),tick函數(shù)里調(diào)用了所有需要check timer & do的事情,然后所有需要check timer & do的事情都塞進(jìn)tick里。

      改進(jìn):tick里調(diào)用了tick50ms、tick100ms、tick500ms,tick1000ms,tick5000ms,然后把需要check timer & do的邏輯根據(jù)精度要求塞到不同的tickXXms里去。

      減法

      減少冗余

      減少拷貝、零拷貝

      減少參數(shù)個(gè)數(shù)(寄存器參數(shù)、取決于ABI約定)

      減少函數(shù)調(diào)用次數(shù)/層次

      減少存儲(chǔ)引用次數(shù)

      減少無效初始化和重復(fù)賦值

      循環(huán)優(yōu)化

      這方面的知識(shí)很多,感覺一下子講不完,提幾點(diǎn),循環(huán)套循環(huán)要內(nèi)大外小,盡量把邏輯提取到循環(huán)外。

      提取與循環(huán)無關(guān)的表達(dá)式,盡量減少循環(huán)內(nèi)不必要計(jì)算。

      循環(huán)內(nèi)盡量使用局部變量。

      循環(huán)展開是一種程序變換,通過增加每次迭代計(jì)算的元素的數(shù)量,減少循環(huán)的迭代次數(shù)。

      還有循環(huán)分塊的騷操作。

      防御性編程適可而止

      有兩個(gè)流派,一個(gè)是完全的不信任,即所有函數(shù)調(diào)用里都對(duì)參數(shù)判斷,包括判空,有效性檢查等,但這樣做有幾點(diǎn)不好:

      第一,它只是貌似更安全,并不是真的更安全。

      第二,它稀釋代碼濃度,淹沒關(guān)鍵語句。

      第三,如果通過返回值報(bào)告錯(cuò)誤,則加重了調(diào)用者負(fù)擔(dān),調(diào)用者需要添加額外代碼檢查,不然更奇怪。

      第四,重復(fù)判斷空耗CPU。

      第五,埋雷,把本該crash或者暴露的問題埋得更深。

      但這種做法大行其道,它有一定的市場(chǎng)和道理。

      另一個(gè)是界定邊界,區(qū)分公開接口和內(nèi)部實(shí)現(xiàn),檢查只在模塊之間進(jìn)行,就相當(dāng)于進(jìn)園區(qū)的時(shí)候,門衛(wèi)會(huì)檢查你證件,但之后,則不再檢查。因?yàn)閮?nèi)部實(shí)現(xiàn)是受控的安全上下文,開發(fā)者應(yīng)該完全cover住。

      我主張防御性編程適可而止,一些著名的開源項(xiàng)目也不會(huì)做過多防御,比如linux kernel、NGINX、skynet等,但現(xiàn)實(shí)中,軟件開發(fā)通常多人合作,每個(gè)開發(fā)者素質(zhì)不一樣,這就是客觀現(xiàn)實(shí),所以我也理解前一種做法。

      release干凈

      開發(fā)過程中,我們會(huì)加很多診斷信息,比如我們可能接管內(nèi)存分配,從而附加額外的首尾部,通過填寫magic Num捕獲異常或者內(nèi)存越界,但這些信息應(yīng)該只用于開發(fā)階段的DEBUG需要,在release階段應(yīng)該通過預(yù)處理的方式刪除掉。

      日志分級(jí)其實(shí)也體現(xiàn)了這種思想,通常有兩種做法,一個(gè)是定義級(jí)別變量,另一個(gè)是預(yù)處理,預(yù)處理干凈,但需要重新編譯生成image,而變量更靈活,但變量的比較還是有開銷的。

      不要忽視這些診斷調(diào)試信息的開銷,牢記不必做的事情絕不做的原則。

      慎用遞歸

      遞歸的寫法簡(jiǎn)單,理解起來也容易,但遞歸是函數(shù)調(diào)用,有棧幀建立撤銷控制跳轉(zhuǎn)的開銷,另外也有爆棧的風(fēng)險(xiǎn),在性能敏感關(guān)鍵路徑,優(yōu)先考慮用非遞歸版本。

      4、編譯優(yōu)化與優(yōu)化選項(xiàng)

      inline

      restrict

      LTO

      PGO

      優(yōu)化選項(xiàng)

      5、其他優(yōu)化

      綁核

      SIMD

      鎖與并發(fā)

      鎖的粒度

      無鎖編程

      Per-cpu data structure & thread local

      內(nèi)存屏障

      異構(gòu)優(yōu)化/TCO優(yōu)化

      比如用GPGPU、FPGA、SmartNIC來offload原來cpu的任務(wù),TCO優(yōu)化指的是不以性能優(yōu)化為單一指標(biāo),而是在滿足性能條件下以綜合成本為優(yōu)化直播,當(dāng)然異構(gòu)也包括主動(dòng)利用CPU的avx或者其他邏輯單元,這類優(yōu)化往往編譯器不能自動(dòng)展開(@zrg)

      常識(shí)和數(shù)據(jù)

      CPU拷貝數(shù)據(jù)一般一秒鐘能做到幾百兆,當(dāng)然每次拷貝的數(shù)據(jù)長(zhǎng)度不同,吞吐不同。

      一次函數(shù)執(zhí)行如果耗費(fèi)超過1000 cycles就比較大了(刨除調(diào)用子函數(shù)的開銷)。

      pthread_mutex_t是futex實(shí)現(xiàn),不用每次都進(jìn)入內(nèi)核,首次加解鎖大概耗時(shí)4000-5000 cycles左右,之后,每次加解鎖大概120 cycles,O2優(yōu)化的時(shí)候100 cycles,spinlock耗時(shí)略少。

      lock內(nèi)存總線+xchg需要50 cycles,一次內(nèi)存屏障要50 cycles。

      有一些無鎖的技術(shù),比如CAS,比如linux kernel里的kfifo,主要利用了整型回繞+內(nèi)存屏障。

      幾個(gè)如何?

      如何定位CPU瓶頸?

      CPU是通常大家最先關(guān)注的性能指標(biāo),宏觀維度有核的CPU使用率,微觀有函數(shù)的CPU cycle數(shù),根據(jù)性能的模型,性能規(guī)格與CPU使用率是互相關(guān)聯(lián)的,規(guī)格越高,CPU使用率越高,但是處理器的性能往往又受到內(nèi)存帶寬、Cache、發(fā)熱等因素的影響,所以CPU使用率和規(guī)格參數(shù)之間并不是簡(jiǎn)單的線性關(guān)系,所以性能規(guī)格翻倍并不能簡(jiǎn)單地翻譯成我們的CPU使用率要優(yōu)化一倍。

      至于CPU瓶頸的定位工具,最有名也是最有用的工具就是perf,它是性能分析的第一步,可以幫我們找到系統(tǒng)的熱點(diǎn)函數(shù)。就像人看病一樣,只知道癥狀是不夠的,需要通過醫(yī)療機(jī)器進(jìn)一步分析病因,才能對(duì)癥下藥。

      所以我們通過性能分析工具PMU或者其他工具去進(jìn)一步分析CPU熱點(diǎn)的原因比如是指令數(shù)本身就比較多,還是Cache miss導(dǎo)致的等,這樣在做性能優(yōu)化的時(shí)候不會(huì)走偏。

      優(yōu)化CPU的目標(biāo)就是讓CPU運(yùn)行不受阻礙。

      如何定位IO瓶頸?

      系統(tǒng)IO的瓶頸可以通過CPU和負(fù)載的非線性關(guān)系體現(xiàn)出來。當(dāng)負(fù)載增大時(shí),系統(tǒng)吞吐量不能有效增大,CPU不能線性增長(zhǎng),其中一種可能是IO出現(xiàn)阻塞。

      系統(tǒng)的隊(duì)列長(zhǎng)度特別是發(fā)送、寫磁盤線程的隊(duì)列長(zhǎng)度也是IO瓶頸的一個(gè)間接指標(biāo)。

      對(duì)于網(wǎng)絡(luò)系統(tǒng)來講,我建議先從外部觀察系統(tǒng)。所謂外部觀察是指通過觀察外部的網(wǎng)絡(luò)報(bào)文交換,可以用tcpdump, wireshark等工具,抓包看一下。

      比如我們優(yōu)化一個(gè)RPC項(xiàng)目,它的吞吐量是10TPS,客戶希望是100TPS。我們使用wireshark抓取TCP報(bào)文流,可以分析報(bào)文之間的時(shí)間戳,響應(yīng)延遲等指標(biāo)來判斷是否是由網(wǎng)絡(luò)引起來的。

      然后可以通過netstat -i/-s選項(xiàng)查看網(wǎng)絡(luò)錯(cuò)誤、重傳等統(tǒng)計(jì)信息。還可以通過iostat查看cpu等待IO的比例。IO的概念也可以擴(kuò)展到進(jìn)程間通信。

      對(duì)于磁盤類的應(yīng)用程序,我們最希望看到寫磁盤有沒有時(shí)延、頻率如何。其中一個(gè)方法就是通過內(nèi)核ftrace、perf-event事件來動(dòng)態(tài)觀測(cè)系統(tǒng)。比如記錄寫塊設(shè)備的起始和返回時(shí)間,這樣我們就可以知道磁盤寫是否有延時(shí),也可以統(tǒng)計(jì)寫磁盤時(shí)間耗費(fèi)分布。有一個(gè)開源的工具包perf-tools里面包含著iolatency, iosnoop等工具。

      如何定位IO瓶頸?

      應(yīng)用程序常用的IO有兩種:Disk IO和網(wǎng)絡(luò)IO。判斷系統(tǒng)是否存在IO瓶頸可以通過觀測(cè)系統(tǒng)或進(jìn)程的CPU的IO等待比例來進(jìn)行,比如使用mpstat、top命令。

      系統(tǒng)的隊(duì)列長(zhǎng)度特別是發(fā)送、寫磁盤線程的隊(duì)列長(zhǎng)度也是IO瓶頸的一個(gè)重要指標(biāo)。

      對(duì)于網(wǎng)絡(luò) IO來講,我們可以先使用netstat -i/-s查看網(wǎng)絡(luò)錯(cuò)誤、重傳等統(tǒng)計(jì)信息,然后使用sar -n DEV 1和sar -n TCP,ETCP 1查看網(wǎng)路實(shí)時(shí)的統(tǒng)計(jì)信息。ss (Socket Statistics)工具可以提供每個(gè)socket相關(guān)的隊(duì)列、緩存等詳細(xì)信息。

      更直接的方法可以用tcpdump, wireshark等工具,抓包看一下。

      對(duì)于Disk IO,我們可以通過iostat -x -p xxx來查看具體設(shè)備使用率和讀寫平均等待時(shí)間。如果使用率接近100%,或者等待時(shí)間過長(zhǎng),都說明Disk IO出現(xiàn)飽和。

      一個(gè)更細(xì)致的觀察方法就是通過內(nèi)核ftrace、perf-event來動(dòng)態(tài)觀測(cè)Linux內(nèi)核。比如記錄寫塊設(shè)備的起始和返回時(shí)間,這樣我們就可以知道磁盤寫是否有延時(shí),也可以統(tǒng)計(jì)寫磁盤時(shí)間耗費(fèi)分布。有一個(gè)開源的工具包perf-tools里面包含著iolatency, iosnoop等工具。

      4.如何定位鎖的問題?

      大家都知道鎖會(huì)引入額外開銷,但鎖的開銷到底有多大,估計(jì)很多人沒有實(shí)測(cè)過,我可以給一個(gè)數(shù)據(jù),一般單次加解鎖100 cycles,spinlock或者cas更快一點(diǎn)。

      使用鎖的時(shí)候,要注意鎖的粒度,但鎖的粒度也不是越小越好,太大會(huì)增加撞鎖的概率,太小會(huì)導(dǎo)致代碼更難寫。

      多線程場(chǎng)景下,如果cpu利用率上不去,而系統(tǒng)吞吐也上不去,那就有可能是鎖導(dǎo)致的性能下降,這個(gè)時(shí)候,可以觀察程序的sys cpu和usr cpu,這個(gè)時(shí)候通過perf如果發(fā)現(xiàn)lock的開銷大,那就沒錯(cuò)了。

      如果程序卡住了,可以用pstack把堆棧打出來,定位死鎖的問題。

      如何提?Cache利用率?

      內(nèi)存/Cache問題是我們常見的負(fù)載瓶頸問題,通常可利用perf等一些通用工具來輔助分析,優(yōu)化cache的思想可以從兩方面來著手,一個(gè)是增加局部數(shù)據(jù)/代碼的連續(xù)性,提升cacheline的利用率,減少cache miss,另一個(gè)是通過prefetch,降低miss帶來的開銷。

      通過對(duì)數(shù)據(jù)/代碼根據(jù)冷熱進(jìn)行重排分區(qū),可提升cacheline的有效利用率,當(dāng)然觸發(fā)false-sharing另當(dāng)別論,這個(gè)需要根據(jù)運(yùn)行trace進(jìn)行深入調(diào)整了;說到prefetch,用過的人往往都有一種體會(huì),現(xiàn)實(shí)效果比預(yù)期差的比較遠(yuǎn),確實(shí)無論是數(shù)據(jù)prefetch還是代碼prefetch,不確定性太大,指望編譯器更靠譜點(diǎn)。

      6、小結(jié)

      性能優(yōu)化是一項(xiàng)細(xì)致的工作,性能優(yōu)化也是一個(gè)系統(tǒng)性工程。性能優(yōu)化通常是在現(xiàn)有系統(tǒng)和代碼基礎(chǔ)上做改進(jìn),它并非推倒重來,考驗(yàn)的是開發(fā)者反向修復(fù)的能力,而性能設(shè)計(jì)考驗(yàn)的是設(shè)計(jì)者的正向設(shè)計(jì)能力,但性能優(yōu)化的方法可以指導(dǎo)性能設(shè)計(jì),兩者互補(bǔ)。

      任務(wù)調(diào)度 數(shù)據(jù)結(jié)構(gòu)

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      版權(quán)聲明:本文內(nèi)容由網(wǎng)絡(luò)用戶投稿,版權(quán)歸原作者所有,本站不擁有其著作權(quán),亦不承擔(dān)相應(yīng)法律責(zé)任。如果您發(fā)現(xiàn)本站中有涉嫌抄襲或描述失實(shí)的內(nèi)容,請(qǐng)聯(lián)系我們jiasou666@gmail.com 處理,核實(shí)后本網(wǎng)站將在24小時(shí)內(nèi)刪除侵權(quán)內(nèi)容。

      上一篇:在excel2010中,在什么功能區(qū)可進(jìn)行工作簿視圖方式的切換(在excel2010中有哪幾種工作簿視圖)
      下一篇:WPS如何快速的建立表格?(WPS怎樣建立表格)
      相關(guān)文章
      亚洲精品乱码久久久久久V| 亚洲欧洲日本精品| 亚洲无人区一区二区三区| 亚洲私人无码综合久久网| 亚洲国产超清无码专区| 久久精品国产亚洲av水果派 | 亚洲男人av香蕉爽爽爽爽| 亚洲av无码成人精品国产| 国产精品亚洲一区二区麻豆| 久久精品国产99国产精品亚洲| 亚洲成a人片7777| 亚洲色图古典武侠| 亚洲另类图片另类电影| 亚洲一级片在线观看| 亚洲日本久久久午夜精品| 亚洲偷自拍另类图片二区| 亚洲高清国产拍精品熟女| 亚洲а∨精品天堂在线| 亚洲AV无码一区二区三区电影| 亚洲人成网址在线观看| 久久亚洲精品人成综合网| 亚洲精品在线视频观看| 亚洲AV无码成人专区| 亚洲日韩精品国产一区二区三区 | 亚洲国产精品久久人人爱| 亚洲国产精品日韩在线观看| 亚洲一区二区久久| 亚洲色大成网站www久久九| 亚洲AV永久无码精品一福利| 内射无码专区久久亚洲| 亚洲精品老司机在线观看| 在线观看亚洲av每日更新| 亚洲综合无码AV一区二区| 久久亚洲精品成人| 久久av无码专区亚洲av桃花岛| 亚洲福利一区二区| 亚洲最大av资源站无码av网址| 国产亚洲真人做受在线观看| 亚洲日韩精品无码一区二区三区| 在线观看亚洲精品国产| 亚洲精品乱码久久久久久按摩|