華文網

獨家|深入淺出講解平行計算性能分析與優化方法(課程精華筆記)

[導讀]工業4.0、人工智慧、大資料對計算規模增長產生了重大需求。

近年來,中國高性能電腦得到突飛猛進的發展,從“天河二號”到“神威·太湖之光”,中國超級電腦在世界Top500連續排名第一。雲計算、人工智慧、大資料的發展對平行計算既是機遇又是挑戰。如何提高應用的性能及擴展性,提高電腦硬體的使用效率,顯得尤為重要。從主流大規模並行硬體到能夠充分發揮其資源性能的並行應用,中間有著巨大的鴻溝。

本次講座由清華-青島資料科學研究院邀請到了北京並行科技股份有限公司研發總監黃新平先生,

從高性能平行計算發展趨勢,到高性能平行計算性能優化基礎,包括從系統級到代碼級分析,常用的優化方法與工具,平行計算優化實戰等方面進行了深入淺出的講解。

黃新平先生長期從事半導體、編譯器和作業系統內核方面的工作,有10年的編譯器設計, 包括GCC、英特爾、摩托摩拉公司的多款編譯器的開發經驗。Solaris 內核的x86 移植和優化,以及國內一些頂級互聯網企業的技術支援工作,如新浪、360、京東、愛奇藝等。2014年加入北京並行科技,擔任研發總監,負責整個產品研發,提供高性能計算的公有雲及私有雲服務。

課程精華筆記

一、高性能和平行計算發展的趨勢

黃新平先生首先闡述了為什麼要關注計算的性能,因為世界上總有一些大問題,還有更多新問題需要大量的計算去解決。比如全基因的排序,精准醫療等應用。石油能源行業的地震勘探資料處理,最終得到油藏資料,價值以億計。

還有一些是沒法做實驗的,比如核子試驗,只能通過計算模擬。在現代化工業製造中,比如汽車製造中需要確定風阻係數,以前需要做模型進行風洞試驗,又貴又慢,而現在做一個電腦CAD模型,用流體類比軟體去計算,就變得非常便宜而且靈活。

2008年奧運會的天氣預報,需要精確到每個場館,而以前是以城市為單位的,精度從百公里縮小到以1公里為單位的網格進行計算,整個計算量提高了不止成千上萬倍,

但計算必須在規定時間期限內算完,所以,必須要以更快的速度去進行計算。所有這種種,都體現了人類對計算性能的需求永無止境。

然後,黃新平先生闡述了CPU體系架構發展的趨勢。

2007年以前,CPU的性能基本等同於頻率,逐年按照指數級曲線增長。但是從2007年開始,CPU的頻率已基本上不再增長,甚至還往下掉,這是因為純粹的頻率的上升導致了CPU功耗急劇加大,最終散熱問題成為瓶頸。但是著名的摩爾定律還是被很好地遵守著,每年仍然不斷按指數級增長的電晶體數目被用來做成CPU內部的多核,進行平行計算,以提高系統的性能。

這些年來,在處理器體系架構技術裡面,對性能影響最大的、程式師可見的,一個是多核和超執行緒技術,另一個就是向量化。所謂向量化,比方說在矩陣相乘的運算中,普通的做法是一個迴圈分別取一行和一列裡面的一個數,做乘加運算。如果先把行和列的數集中放在一個比較寬的寄存器裡面,比如512位的寄存器可以放16個32位的單精確度浮點數,一個乘加指令就能完成對這16個數的計算,從而得到16倍的速度提高。

還有一些是程式師不可見的底層技術,一個是指令流水線,類似於工廠生產流水線,多個指令在不同的執行階段被同時處理,提高了吞吐率。一個是超標量,這個技術就是有多條指令同時在執行單元被執行。還有一個是亂序執行技術。指令按循序執行的時候,假設其中有一條指令需要讀取資料,一般來講,從一級快取記憶體中取得資料需要經歷16個時鐘週期左右的時間,在等待資料取回的時間裡面,它後面的指令需要跟它一起等,這顯然是極大的浪費。

如果後面的指令沒有用到這個資料的話,實際上完全不用等,所以可以設計一個亂序執行機制,把一批指令都扔到執行單元裡面去算。如果不需要等資料的指令就可以直接把結果算出來,在這個過程中需要等的資料到了,那條指令就可以執行了,然後按照指令順序,一條一條指令完成退出,最終的執行結果還是按順序的,但是執行的過程是亂序的。

亂序過程提高了整個運算單元的硬體利用率,掩蓋掉慢速指令執行導致的延遲,能夠帶來非常大的性能增長。《電腦體系結構 - 量化研究方法》一書中有專門的章節講亂序執行技術,如果大家感興趣的話可以找來看看。在wiki上搜索Tomasulo演算法也有比較詳細的解釋。

另外一個影響比較大的是集成記憶體控制器,原來記憶體控制器是做在主機板上,現在做在CPU裡面,這樣使記憶體訪問的速度比以前快很多,大幅度降低了延遲,提高了記憶體訪問的速度。現實中CPU運算速度的提高是遠遠高於記憶體存取速度的提高的,這兩者之間的差距越來越大,大多數指令執行僅需要一個時鐘週期,而伺服器記憶體的資料訪問則大約需要300個時鐘週期左右。

有很多的應用性能不高,就是因為有大量集中的記憶體訪問,由於記憶體的速度跟不上計算速度,所以處理器不得不停下來等待資料到達。除了加大快取記憶體的容量之外,還有一個辦法就是增加高頻寬的近核記憶體。例如大家比較熟悉的GPU,當處理器系統還在使用DDR3記憶體的時候,GPU早就使用DDR5記憶體了。

黃新平先生指出,如果不利用多核來寫多執行緒的程式,以及執行緒中沒有利用向量化指令來做運算,例如實測使用英特爾至強CPU的伺服器,2007年和2014年相比,跑單執行緒並且沒有使用向量化指令的程式,性能幾乎沒有增長。而經過多執行緒並行化和向量化調優之後,性能就會有102倍的提高了。

黃新平先生同時詳細介紹了Intel近年推出的眾核新產品Xeon Phi KNL。

在不斷追求高性能的處理器當中,英特爾推出被稱為眾核處理器的一系列產品。第一代產品的代號是KNC。在全球高性能系統中,連續三年排名第一的天河二號,就使用了KNC。KNC看起來像GPU,插在PCIe 插槽上。一塊KNC已經做到了1 TFlops的浮點性能, 也就是每秒鐘可以執行1T條雙精度浮點型運算指令。而現在的KNL一塊CPU就可以達到3 TFlops。它的最高配置達到72個核,每個核4個執行緒,每個核有兩個512位元的向量處理單元。另外它的封裝裡面帶了一個16G的近核記憶體,讀寫頻寬高達400GB每秒以上,系統DDR4記憶體的頻寬達到90GB每秒。

GPU是圖形處理單元,增加了通用處理功能後,被稱為GPGPU。它的設計思路與上面所說的類似,通過使用高頻寬記憶體減少記憶體的訪問延遲,使用數以千計的大量向量處理單元並行處理資料,從而獲得極高的性能。現在GPGPU在機器學習裡用得較多,GPGPU也針對機器學習做了較多的優化,比如降低浮點數精度,16位的浮點數,因為很多演算法對精度要求很低,從而進一步提高了處理速度。

黃新平先生提出在高性能/平行計算系統越來越碎片化和普遍採用異構架構的今天,性能調優顯得尤其重要。

從發展趨勢來看,高性能運算的機器越來越普及,越來越碎片化。最早從巨型機,分散式處理機、向量機,到最後走向集群,而現在出現GPGPU之後,單台伺服器,甚至是一台筆記本的性能就可以媲美幾年前看起來很強大的高性能運算中心。高性能平行計算不再遠在天邊,而在你的指尖。第二個變化是異構架構的普及,輔助處理器,GPGPU,FPGA等高性能處理部件飛入尋常百姓家。在這樣兩個大趨勢下,高性能和平行計算程式設計、調優不再是一個很遙遠的事情,而是一個必須每天要面對的事情。

黃新平先生同時指出平行計算程式設計常用的有兩個技術,一是OpenMP技術,一是MPI技術。

針對單台伺服器,準確地說是共用記憶體系統,充分利用多核、多執行緒的並行處理能力,通常使用OpenMP技術。對於大量的資料做類似的處理的應用,通常在程式設計中使用計算密集迴圈來完成資料處理。這個迴圈一般就可以通過OpenMP 技術,添加編譯器指導指令使其自動變成一個多執行緒程式,每個執行緒其中一部分資料,在執行完以後自動把結果收攏起來,得到最終結果,這樣就能充分利用多核的處理性能了。

對於問題規模超過單節點處理能力的應用,可以使用MPI技術。利用很多台機器同時運算,比如天河二號上面有的應用需要使用上百萬個核做處理,顯然不可能有一台機器可以擁有100萬個核,那麼當使用這麼多台機器一起處理資料的時候,一個重要的問題就是要通過網路互聯來交換資料。如果自己從底層開始編寫網路交換代碼是不現實的,你會發現大部分時間都在調試各種網路問題,沒有時間寫演算法了。而有一個通用的標準API把它封裝起來,做這些底層工作,這個API就叫做MPI,用於發送資訊和接收資訊,實現資料交換。

異構程式的開發也比幾年前好了很多。英特爾的KNL的開發就跟普通的CPU程式設計沒有區別,針對GPGPU程式設計也可以使用CUDA或者OpenCL,這些都大幅度降低了開發的難度。因為時間的原因這裡就不具體展開程式設計方面的介紹了,有興趣的可以參閱相關書籍。

二、高性能和平行計算優化基礎

黃新平先生首先介紹了決定性能調優上限的兩個定律:艾曼達定律和Gustafson定律。

艾曼達定律說的是,如果一個程式包括並行和串列,隨著機器數量增加,並存執行時間會越來越短,最後趨向於0,串列的時間沒有變,這就是加速比,如果串列部分占到了整個執行時間的50%,意味著加到1024台機器也只能加速一倍,非常浪費,哪怕並行的部分占到90%,其實也就是加速了9倍。這個講得是當工作量是固定的時候,可以並行處理的部分所占比例越高越好,描述的是程式的強可擴展性特性。

Gustafson定律說得是,在不斷增加處理工作量的情形下,增加系統的規模是有用的。比如天氣預報計算,如果增加了1萬倍的處理量,又必須在規定時間之內算完,怎麼辦。很多情況下,工作規模增加的部分都是可以並行處理的部分,因此增加系統規模自然會縮短處理時間。這是一個非常樂觀的定律,也就是說彙聚了很多機器一定能幹更大的事。

艾曼達定律描述了隨著增加更多的處理器,串列部分處理沒變,只是並行部分的執行時間會不斷減少,但總的加速比是受串列部分所占比例限制的。而Gustafson定律則描述了隨著工作量的增加,加上更多的處理器單元,可以縮短並行處理的時間,從而在規定時間內,處理的工作量增加了。

黃新平先生然後介紹了調優的方法論:

調優是一個看起來很龐雜、很細緻的工作。需要一個方法論做指導,才會事半功倍。調優過程是這樣一個迴圈,首先要找到一個典型的、可重複的測試用例,然後使用這個測試用例得到一個基準,記錄這個基準。在基準的測試過程中,需要收集大量的性能資料,這些性能資料將會指出問題所在。

系統的性能遵循木桶原理,也就是整體性能是由系統中最短的那塊板決定的,在性能資料收集的過程中可以通過性能指標發現哪個地方有問題,根據具體問題就可以找到相應的解決方案來解決。使用解決方案解決這個問題之後,再重新測試收集,找到下一塊短板。以此循環往復,直至性能達到期望或者無法繼續增進為止。

整個優化應該採用自上而下的方法,順序一定不能亂。首先通過標準性能基準測試程式確保系統的工作狀態正常, 比如使用SpecInt, SpecFP,Linpack等得到處理器的性能,對比設計性能指標,可以得知CPU是否工作正常,BIOS或者作業系統中的相關設置是否正確。使用Stream測試程式測試記憶體系統的性能,Netperf測試網路性能,Fio或者iozone等測試檔案系統性能是否正常。在所有調優開始之前,一定要先把基礎做好,一定要瞭解你的系統性能極限。

再來是應用調優,可以調節運行環境,或者有代碼的可以調整代碼,最後才會到處理器級別的調優,這裡是榨幹最後一滴性能的地方。

比如說我發現記憶體訪問是一個問題,第一個步驟是檢查硬體設置,伺服器的記憶體插法是有黑魔法的,怎麼插,插在不同位置上,性能是不一樣的,BIOS記憶體訪問方式設置也會有比較大的影響,可以翻看相關伺服器的產品手冊。

另外記憶體控制器是有不同通道的,每個通道速度不一樣,而且一般來講當記憶體所有通道都被插滿記憶體條的時候,記憶體會被自動降頻,因此每CPU使用單條大記憶體的性能會超過同樣記憶體總容量下,所有記憶體插槽都插滿了的小記憶體的速度。當伺服器有多個處理器存在的時候,通常是一個非對稱統一記憶體位址訪問的架構(NUMA架構),也就是說每個CPU的記憶體控制器都有可能掛著自己的記憶體,它離得最近,別的CPU就遠,因此記憶體是有距離的。BIOS中設置成對稱多處理(SMP),記憶體編址將會混合遠近不同的實體記憶體位址,達到均勻訪問的目的,但是會損失性能。而現在幾乎所有的主流作業系統都是NUMA 感知的,能充分利用NUMA架構,盡可能使用近距離記憶體以提高性能。

硬體調整做完之後還有軟體調整,比如作業系統裡調整記憶體頁的大小,缺省的4K一個的小頁在海量記憶體容量的情況下,意味著頁表條目非常巨大。會增加DTLB Miss的機率,導致系統忙於裝載切換DTLB和記憶體頁,從而損失性能。

在應用代碼中最主要相關的是資料結構和演算法。比如說,某一點的座標float X,Y,Z,有大量的點需要處理,而有時候會用不同的方式分別處理X,Y,Z,假設定義一個結構,包含X,Y,Z三個值,然後再定義一個結構的陣列,每個元素是一個結構,如果這麼處理的話,處理X值的任務是怎麼訪問記憶體的呢?隔一段距離跳著訪問,記憶體訪問的效率是很低的,尤其是快取記憶體的利用率,還會產生所謂假共用(false sharing)的性能問題。

這裡有一個改進辦法,就是定義一個結構,包含三個陣列,所有X放在一個陣列,所有Y放在一個陣列,所有Z放在一個陣列。它有幾個好處,首先,這樣所有的X值就連續放在一起了,儘管在程式中訪問記憶體的時候是可以按位元組存取的,但是CPU在實際執行的時候是以快取記憶體線(cache line)最小單位存取的,緩存線一般是64個位元組大小,第一次讀取X的第一個數的時候,實際上一次就取64個位元組上去,第二個數也被讀取了,因此訪問第二個數的時候,在快取記憶體線中,可以大幅度節省時間。還有因為特別有規律,一個一個連取,CPU內部會自動預取資料,它會在使用之前就把資料準備好,這個資料結構的更改會大幅度提高性能。

同樣因為按快取記憶體線為單位存取的原因,假如處理過程中都需要修改X,Y,Z的值,在原來基於結構的陣列的情況下,處理X資料的任務,修改了X值,會導致同一個點的Y值在快取記憶體中也被標記為修改了,因為它們在同一個快取記憶體線中,會導致處理Y的任務,在讀取Y值的時候,被迫刷新快取記憶體線,從記憶體中重新讀取資料,這就是所謂的假共用問題,會導致性能急劇下降。

黃新平先生介紹了性能調優的方法分類:硬體級、運行級、編譯器級和代碼級。

(一)硬體級

性能優化的方法有很多種,第一個叫硬體級調優,就是簡單粗暴直接換掉性能低的硬體,比如網卡從千兆換到萬兆的,硬碟從機械的換成SSD等等。很多時候這也不失為一個好辦法。

(二)運行級

另外一個所謂運行級調優,是從運行環境上調整,通過監控整個系統的性能及各項指標看問題所在,然後看能不能通過一些運行參數的調整,比如說記憶體的使用率非常高,可以試試在作業系統中調整記憶體頁的大小。如果是網路頻寬壓力特別大,可以試試將網路包的處理常式綁定在某一個核上面。對於網路小包特別多的情況,有一些網卡帶包聚合功能,等很多小包會聚合到一定的程度,再統一處理,大量減少中斷數量,降低系統消耗。這些調整的成本很低,難點在於對技術人員要求很高,需要對整個系統非常熟。

(三)編譯器級

還有編譯器級調優,需要有代碼,但是不修改代碼,使用編譯器的優化選項,有的時候也能夠獲得巨大的性能提高。比如引入自動向量化,深度優化,性能剖析指導的優化(PGO)等等。需要技術人員熟悉編譯的使用,以及對優化過程的理解。

(四)代碼級

還有代碼級調優,也就是直接改動實現代碼,改代碼收益或許非常大,比如換個演算法,因為有的演算法就是比別的演算法快很多倍,甚至快幾個數量級,但是修改的難度極其大,成本極高,代碼改了以後正確性要重新驗證,所有測試的步驟都要做。

在原有串列單執行緒程式中,如果有比較明顯的計算密集型迴圈,可以引入OpenMP進行並行化,結合編譯器的自動向量化編譯選項,可以只改極小一部分代碼,獲得比較大的性能收益。

黃新平先生緊接著介紹了在性能調優中常用的一些關鍵指標:系統性能指標包括CPU利用率、記憶體使用率,swap分區、頻寬使用率;處理器性能指標包括CPI、浮點運算的峰值、向量化比例、cache miss、DTLB miss等。

第一個,CPU利用率,CPU利用率其實內部包括了幾個方面,光看利用率數值是不夠的,比如CPU利用率100%,但是system time占了30%,這樣其實性能並不好,user time是CPU跑使用者程式的時間,越高越好,而system time 是消耗在內核裡面的時間,通常是由網路、磁片的一些中斷導致的,更常見的是鎖。還有一個是io wait,當大量磁片讀取的時候就會有比較明顯的占比。

還有一個是swap的使用, 當使用到swap的時候會感覺很慢,幾乎沒有回應了,這是為什麼?是因為硬碟的訪問及其緩慢。 大家通常對特別小的資料沒有太多概念。一般來講,CPU取第一級Cache典型的時間是十來個時鐘週期,如果按2G赫茲算的話是幾個納秒,而硬碟訪問的延遲大概是幾十個毫秒。有一個很形象的比喻,如果從一級緩存取數需要1秒的時間的話,那麼從硬碟中讀取資料就需要116天。

另外一個是網路,網路分為千兆網、萬兆網、infiniband網路,這個千兆網就是1000比特每秒,萬兆就是10000比特每秒。infiniband是一個非常快速的網路,它能達到40G到100G。

到了CPU裡面,有一個重要的指標叫CPI(Cycles Pre Instruction),就是每條指令需要多少個時鐘週期完成,現在主流處理器的理論值是0.25,因為它有4個算術邏輯運算單元。實際上測下來,比如Linpack能夠達到0.33。為什麼CPI會升高,因為指令有記憶體訪問的時候它不得不等待,雖然亂序執行能夠掩蓋一部分延遲,但是不可能完全填補運算速度與記憶體訪問直接的差距,這個指標值就上來了。

另外一個指標是浮點運算的峰值,如果大家關心過GPU的一般都會知道這個數,每秒鐘執行多少浮點型運算。實際運行的浮點型運算的性能值和理論峰值之間的比值就是浮點運算單元的使用率,很不幸的是並行科技運維過很多計算中心,發現絕大多數應用的這個使用率小於10%, 也就是說買了一個160公里時速的車永遠以10多公里的時速開, 問題基本都在軟體層面上。有時候優化過後,就可以達到60%到70%多,一下子性能提升了六七倍。

向量化比例是剛才浮點型運算效率偏低的原因,因為沒用向量化,還在用單個標量運算,肯定性能比較爛,利用向量化之後,一條指令執行16-32條資料就處理完了,而標量計算才處理一個,很多時候就是改一改編譯器的選項就完成了。

Cache miss比例是指數據訪問的時候沒有在快取記憶體裡找到的,需要到記憶體中去取,肯定會帶來較大的性能損失,這個miss比率對性能的影響比較大, 性能好的時候是很低的資料,一般要小於10%。

還有一個是DTLB miss。進程訪問的位址空間是邏輯位址空間,比如64位元的程式,位址範圍從0到2的64次方減一,然而這麼大的位址空間,實際上不可能配置這麼多實體記憶體的,需要建立一個從邏輯記憶體到實體記憶體的映射表,這個映射表的管理是按照頁為最小單位的,缺省的頁大小是4K。但是頁大小可調,越大的頁,頁表條目越少,頁大小為4M的跟4k的頁表條目就差了1024倍。這些頁表不可能完全放在CPU裡面,CPU只能放在一個稱為DTLB的緩衝區中,更多的部分放在記憶體裡面,當映射相應條目的時候,如果在緩衝區找不到就會產生DTLB miss的異常,這個異常處理會從記憶體中找到相關條目,然後放入DTLB緩衝區。這個操作極其耗時,經常發生時對性能影響極大,通常可以通過調整頁大小,或者在程式中使用記憶體池等記憶體管理技術減少耗時。

還有像記憶體頻寬,記憶體頻寬體現了記憶體訪問的忙閑程度,這個值高到一定程度,會導致記憶體延遲迅速增加,有一些工具比如並行的Paramon和Intel的VTune可以幫助測量這個值。

三、並行應用調優工具

工欲善其事必先利其器,工具按照剛才調優的級別會有不同,一個是系統級的分析,包括CPU、GPU、網路、記憶體、swap等。一是應用級分析,包括代碼中各函數的耗時、MPI計算與通信占比等。

並行科技的Paramon應用性能收集監控工具,會即時收集大量性能指標,即時顯示並分析常見性能問題,並將這些資料記錄下來,供Paratune應用性能分析工具進行詳細分析,Paratune可以即時重播集群各伺服器性能指標,並且將這一段的運行特徵分析出來,以四個象限展示,是CPU密集型、記憶體訪問密集型、磁片密集型還是網路密集型一目了然。這是並行最早的產品,現在這一塊作為HPC雲計算服務的一個基礎的工具存在,我們給客戶服務的時候全是拿這些東西做分析。建立了一個大量的運行特徵庫,可以直接指導應用的調優方向及匹配的硬體伺服器的選型與採購。

英特爾的工具VTune,這是一個調優神器,實際用起來操作很簡單,難在它給出了一大堆報告和資料之後怎麼樣解讀它,怎麼樣利用它。它可以迅速定位應用中最耗時的部分以及發現並行程式中不正確的同步導致的性能損失。直接對應到代碼幫助調優者迅速找到原因。

代碼級性能優化即代碼現代化的方法:

代碼級的性能優化,有一個非常好的,就是稱為代碼現代化的一系列方法。 最主要的有並行化,就是要多執行緒化,充分利用多核資源;另外一個是向量化,充分利用處理器向量位元寬,實現單指令多資料的處理;還有是記憶體訪問優化,在KNL或者GPU這樣的有高速高頻寬記憶體的時候,需要充分利用這些資源,將最常訪問的資料與代碼放入高頻寬記憶體區域,縮小處理器處理速度與記憶體存取速度之間的差距。

四、並行應用調優實戰

黃新平先生通過演示矩陣相乘程式闡述編譯參數的調整進行性能調優。

不改變代碼,僅利用編譯器參數進行調優。為我們演示了2048乘以2048的矩陣相乘的演算法。僅僅通過編譯器編譯選項的調整就可以大幅度提高運行效率。執行時間從25秒縮短到0.14秒,性能提高178倍,卻不需要改一行代碼。

黃新平先生通過Black Sholes演算法程式的調優過程演示了少量改變部分代碼的方式進行性能調優。

Black Sholes演算法分了兩個部分,第一個部分用亂數產生器產生大量亂數,模擬買賣單。 第二個部分利用類比資料計算當前的投資價值。首先用VTune尋找問題, 先找熱點,發現指數運算函數、對數運算函數加上一個亂數產生器函數被大量調用。而且它是一個單執行緒的程式,所以第一件事就是在類比計算部分的計算密集的for迴圈處加了OpenMP編譯指令,同時使用編譯器的自動向量化編譯選項,獲得了4倍的性能提升。繼續考察程式,發現初始化部分的亂數產生器, 在英特爾的MKL庫裡有一個非常好的實現,因此可以直接換上這個實現,最終總體程式獲得了22.8倍的性能提升。

演示中跑50萬個 模擬,三次反覆運算。基準性能資料是平均1472個時鐘週期做一個類比,初始化用了1073個時鐘週期,類比計算花了399個時鐘週期。使用OpenMP和向量化指令優化後, 總時間變成了364個時鐘週期,初始化用了338個,計算用了26個。換成MKL庫的亂數產生函數後,總時間變成了64, 初始化用了35,計算用了29。在一個雙核的筆記本上,計算時間從1472變成64,22.8倍的提升,實際代碼量改動非常小。如果在更多核的伺服器上將會有更大的性能提升。

問答精選

Q:Paramon性能採集工具是怎樣採集CPU以外的性能資料的,採集程式是否影響結果的準確性?

A:Paramon性能採集工具不需要修改運行程式,它對運行指標的收集都是從內核的資料結構裡面提取的,不會影響被監測程式實際的運行,額外負載非常低,通常單核在0.5%以下。

Q:您一直講得是比較靠近底層的,我們現在做人工智慧、深度學習,很多人並不會直接學習底層,如python語言,您今天講得調優有什麼幫助?

A:

Q:您今天講的平行計算裡面的一些技術方法主要以CPU為主,GPU可不可以運用?

A:可以的。平行計算在方法論上沒有任何區別, 但是GPU有自己的特點,需要針對這些特點做相應調整,比如GPU有更大規模的硬體執行緒,在使用上需要更好地劃分並行任務和並行資料集,以充分並行化利用硬體資源,在寫GPU程式的時候,非常常見的並行程式設計模式是流水線模式,也可以考慮使用函數式程式設計的思想,函數程式設計很容易實現並行。另外GPU相對CPU來講記憶體的容量較小,PCIe資料傳輸的速度很慢,要考慮好資料的傳輸問題。

最終散熱問題成為瓶頸。但是著名的摩爾定律還是被很好地遵守著,每年仍然不斷按指數級增長的電晶體數目被用來做成CPU內部的多核,進行平行計算,以提高系統的性能。

這些年來,在處理器體系架構技術裡面,對性能影響最大的、程式師可見的,一個是多核和超執行緒技術,另一個就是向量化。所謂向量化,比方說在矩陣相乘的運算中,普通的做法是一個迴圈分別取一行和一列裡面的一個數,做乘加運算。如果先把行和列的數集中放在一個比較寬的寄存器裡面,比如512位的寄存器可以放16個32位的單精確度浮點數,一個乘加指令就能完成對這16個數的計算,從而得到16倍的速度提高。

還有一些是程式師不可見的底層技術,一個是指令流水線,類似於工廠生產流水線,多個指令在不同的執行階段被同時處理,提高了吞吐率。一個是超標量,這個技術就是有多條指令同時在執行單元被執行。還有一個是亂序執行技術。指令按循序執行的時候,假設其中有一條指令需要讀取資料,一般來講,從一級快取記憶體中取得資料需要經歷16個時鐘週期左右的時間,在等待資料取回的時間裡面,它後面的指令需要跟它一起等,這顯然是極大的浪費。

如果後面的指令沒有用到這個資料的話,實際上完全不用等,所以可以設計一個亂序執行機制,把一批指令都扔到執行單元裡面去算。如果不需要等資料的指令就可以直接把結果算出來,在這個過程中需要等的資料到了,那條指令就可以執行了,然後按照指令順序,一條一條指令完成退出,最終的執行結果還是按順序的,但是執行的過程是亂序的。

亂序過程提高了整個運算單元的硬體利用率,掩蓋掉慢速指令執行導致的延遲,能夠帶來非常大的性能增長。《電腦體系結構 - 量化研究方法》一書中有專門的章節講亂序執行技術,如果大家感興趣的話可以找來看看。在wiki上搜索Tomasulo演算法也有比較詳細的解釋。

另外一個影響比較大的是集成記憶體控制器,原來記憶體控制器是做在主機板上,現在做在CPU裡面,這樣使記憶體訪問的速度比以前快很多,大幅度降低了延遲,提高了記憶體訪問的速度。現實中CPU運算速度的提高是遠遠高於記憶體存取速度的提高的,這兩者之間的差距越來越大,大多數指令執行僅需要一個時鐘週期,而伺服器記憶體的資料訪問則大約需要300個時鐘週期左右。

有很多的應用性能不高,就是因為有大量集中的記憶體訪問,由於記憶體的速度跟不上計算速度,所以處理器不得不停下來等待資料到達。除了加大快取記憶體的容量之外,還有一個辦法就是增加高頻寬的近核記憶體。例如大家比較熟悉的GPU,當處理器系統還在使用DDR3記憶體的時候,GPU早就使用DDR5記憶體了。

黃新平先生指出,如果不利用多核來寫多執行緒的程式,以及執行緒中沒有利用向量化指令來做運算,例如實測使用英特爾至強CPU的伺服器,2007年和2014年相比,跑單執行緒並且沒有使用向量化指令的程式,性能幾乎沒有增長。而經過多執行緒並行化和向量化調優之後,性能就會有102倍的提高了。

黃新平先生同時詳細介紹了Intel近年推出的眾核新產品Xeon Phi KNL。

在不斷追求高性能的處理器當中,英特爾推出被稱為眾核處理器的一系列產品。第一代產品的代號是KNC。在全球高性能系統中,連續三年排名第一的天河二號,就使用了KNC。KNC看起來像GPU,插在PCIe 插槽上。一塊KNC已經做到了1 TFlops的浮點性能, 也就是每秒鐘可以執行1T條雙精度浮點型運算指令。而現在的KNL一塊CPU就可以達到3 TFlops。它的最高配置達到72個核,每個核4個執行緒,每個核有兩個512位元的向量處理單元。另外它的封裝裡面帶了一個16G的近核記憶體,讀寫頻寬高達400GB每秒以上,系統DDR4記憶體的頻寬達到90GB每秒。

GPU是圖形處理單元,增加了通用處理功能後,被稱為GPGPU。它的設計思路與上面所說的類似,通過使用高頻寬記憶體減少記憶體的訪問延遲,使用數以千計的大量向量處理單元並行處理資料,從而獲得極高的性能。現在GPGPU在機器學習裡用得較多,GPGPU也針對機器學習做了較多的優化,比如降低浮點數精度,16位的浮點數,因為很多演算法對精度要求很低,從而進一步提高了處理速度。

黃新平先生提出在高性能/平行計算系統越來越碎片化和普遍採用異構架構的今天,性能調優顯得尤其重要。

從發展趨勢來看,高性能運算的機器越來越普及,越來越碎片化。最早從巨型機,分散式處理機、向量機,到最後走向集群,而現在出現GPGPU之後,單台伺服器,甚至是一台筆記本的性能就可以媲美幾年前看起來很強大的高性能運算中心。高性能平行計算不再遠在天邊,而在你的指尖。第二個變化是異構架構的普及,輔助處理器,GPGPU,FPGA等高性能處理部件飛入尋常百姓家。在這樣兩個大趨勢下,高性能和平行計算程式設計、調優不再是一個很遙遠的事情,而是一個必須每天要面對的事情。

黃新平先生同時指出平行計算程式設計常用的有兩個技術,一是OpenMP技術,一是MPI技術。

針對單台伺服器,準確地說是共用記憶體系統,充分利用多核、多執行緒的並行處理能力,通常使用OpenMP技術。對於大量的資料做類似的處理的應用,通常在程式設計中使用計算密集迴圈來完成資料處理。這個迴圈一般就可以通過OpenMP 技術,添加編譯器指導指令使其自動變成一個多執行緒程式,每個執行緒其中一部分資料,在執行完以後自動把結果收攏起來,得到最終結果,這樣就能充分利用多核的處理性能了。

對於問題規模超過單節點處理能力的應用,可以使用MPI技術。利用很多台機器同時運算,比如天河二號上面有的應用需要使用上百萬個核做處理,顯然不可能有一台機器可以擁有100萬個核,那麼當使用這麼多台機器一起處理資料的時候,一個重要的問題就是要通過網路互聯來交換資料。如果自己從底層開始編寫網路交換代碼是不現實的,你會發現大部分時間都在調試各種網路問題,沒有時間寫演算法了。而有一個通用的標準API把它封裝起來,做這些底層工作,這個API就叫做MPI,用於發送資訊和接收資訊,實現資料交換。

異構程式的開發也比幾年前好了很多。英特爾的KNL的開發就跟普通的CPU程式設計沒有區別,針對GPGPU程式設計也可以使用CUDA或者OpenCL,這些都大幅度降低了開發的難度。因為時間的原因這裡就不具體展開程式設計方面的介紹了,有興趣的可以參閱相關書籍。

二、高性能和平行計算優化基礎

黃新平先生首先介紹了決定性能調優上限的兩個定律:艾曼達定律和Gustafson定律。

艾曼達定律說的是,如果一個程式包括並行和串列,隨著機器數量增加,並存執行時間會越來越短,最後趨向於0,串列的時間沒有變,這就是加速比,如果串列部分占到了整個執行時間的50%,意味著加到1024台機器也只能加速一倍,非常浪費,哪怕並行的部分占到90%,其實也就是加速了9倍。這個講得是當工作量是固定的時候,可以並行處理的部分所占比例越高越好,描述的是程式的強可擴展性特性。

Gustafson定律說得是,在不斷增加處理工作量的情形下,增加系統的規模是有用的。比如天氣預報計算,如果增加了1萬倍的處理量,又必須在規定時間之內算完,怎麼辦。很多情況下,工作規模增加的部分都是可以並行處理的部分,因此增加系統規模自然會縮短處理時間。這是一個非常樂觀的定律,也就是說彙聚了很多機器一定能幹更大的事。

艾曼達定律描述了隨著增加更多的處理器,串列部分處理沒變,只是並行部分的執行時間會不斷減少,但總的加速比是受串列部分所占比例限制的。而Gustafson定律則描述了隨著工作量的增加,加上更多的處理器單元,可以縮短並行處理的時間,從而在規定時間內,處理的工作量增加了。

黃新平先生然後介紹了調優的方法論:

調優是一個看起來很龐雜、很細緻的工作。需要一個方法論做指導,才會事半功倍。調優過程是這樣一個迴圈,首先要找到一個典型的、可重複的測試用例,然後使用這個測試用例得到一個基準,記錄這個基準。在基準的測試過程中,需要收集大量的性能資料,這些性能資料將會指出問題所在。

系統的性能遵循木桶原理,也就是整體性能是由系統中最短的那塊板決定的,在性能資料收集的過程中可以通過性能指標發現哪個地方有問題,根據具體問題就可以找到相應的解決方案來解決。使用解決方案解決這個問題之後,再重新測試收集,找到下一塊短板。以此循環往復,直至性能達到期望或者無法繼續增進為止。

整個優化應該採用自上而下的方法,順序一定不能亂。首先通過標準性能基準測試程式確保系統的工作狀態正常, 比如使用SpecInt, SpecFP,Linpack等得到處理器的性能,對比設計性能指標,可以得知CPU是否工作正常,BIOS或者作業系統中的相關設置是否正確。使用Stream測試程式測試記憶體系統的性能,Netperf測試網路性能,Fio或者iozone等測試檔案系統性能是否正常。在所有調優開始之前,一定要先把基礎做好,一定要瞭解你的系統性能極限。

再來是應用調優,可以調節運行環境,或者有代碼的可以調整代碼,最後才會到處理器級別的調優,這裡是榨幹最後一滴性能的地方。

比如說我發現記憶體訪問是一個問題,第一個步驟是檢查硬體設置,伺服器的記憶體插法是有黑魔法的,怎麼插,插在不同位置上,性能是不一樣的,BIOS記憶體訪問方式設置也會有比較大的影響,可以翻看相關伺服器的產品手冊。

另外記憶體控制器是有不同通道的,每個通道速度不一樣,而且一般來講當記憶體所有通道都被插滿記憶體條的時候,記憶體會被自動降頻,因此每CPU使用單條大記憶體的性能會超過同樣記憶體總容量下,所有記憶體插槽都插滿了的小記憶體的速度。當伺服器有多個處理器存在的時候,通常是一個非對稱統一記憶體位址訪問的架構(NUMA架構),也就是說每個CPU的記憶體控制器都有可能掛著自己的記憶體,它離得最近,別的CPU就遠,因此記憶體是有距離的。BIOS中設置成對稱多處理(SMP),記憶體編址將會混合遠近不同的實體記憶體位址,達到均勻訪問的目的,但是會損失性能。而現在幾乎所有的主流作業系統都是NUMA 感知的,能充分利用NUMA架構,盡可能使用近距離記憶體以提高性能。

硬體調整做完之後還有軟體調整,比如作業系統裡調整記憶體頁的大小,缺省的4K一個的小頁在海量記憶體容量的情況下,意味著頁表條目非常巨大。會增加DTLB Miss的機率,導致系統忙於裝載切換DTLB和記憶體頁,從而損失性能。

在應用代碼中最主要相關的是資料結構和演算法。比如說,某一點的座標float X,Y,Z,有大量的點需要處理,而有時候會用不同的方式分別處理X,Y,Z,假設定義一個結構,包含X,Y,Z三個值,然後再定義一個結構的陣列,每個元素是一個結構,如果這麼處理的話,處理X值的任務是怎麼訪問記憶體的呢?隔一段距離跳著訪問,記憶體訪問的效率是很低的,尤其是快取記憶體的利用率,還會產生所謂假共用(false sharing)的性能問題。

這裡有一個改進辦法,就是定義一個結構,包含三個陣列,所有X放在一個陣列,所有Y放在一個陣列,所有Z放在一個陣列。它有幾個好處,首先,這樣所有的X值就連續放在一起了,儘管在程式中訪問記憶體的時候是可以按位元組存取的,但是CPU在實際執行的時候是以快取記憶體線(cache line)最小單位存取的,緩存線一般是64個位元組大小,第一次讀取X的第一個數的時候,實際上一次就取64個位元組上去,第二個數也被讀取了,因此訪問第二個數的時候,在快取記憶體線中,可以大幅度節省時間。還有因為特別有規律,一個一個連取,CPU內部會自動預取資料,它會在使用之前就把資料準備好,這個資料結構的更改會大幅度提高性能。

同樣因為按快取記憶體線為單位存取的原因,假如處理過程中都需要修改X,Y,Z的值,在原來基於結構的陣列的情況下,處理X資料的任務,修改了X值,會導致同一個點的Y值在快取記憶體中也被標記為修改了,因為它們在同一個快取記憶體線中,會導致處理Y的任務,在讀取Y值的時候,被迫刷新快取記憶體線,從記憶體中重新讀取資料,這就是所謂的假共用問題,會導致性能急劇下降。

黃新平先生介紹了性能調優的方法分類:硬體級、運行級、編譯器級和代碼級。

(一)硬體級

性能優化的方法有很多種,第一個叫硬體級調優,就是簡單粗暴直接換掉性能低的硬體,比如網卡從千兆換到萬兆的,硬碟從機械的換成SSD等等。很多時候這也不失為一個好辦法。

(二)運行級

另外一個所謂運行級調優,是從運行環境上調整,通過監控整個系統的性能及各項指標看問題所在,然後看能不能通過一些運行參數的調整,比如說記憶體的使用率非常高,可以試試在作業系統中調整記憶體頁的大小。如果是網路頻寬壓力特別大,可以試試將網路包的處理常式綁定在某一個核上面。對於網路小包特別多的情況,有一些網卡帶包聚合功能,等很多小包會聚合到一定的程度,再統一處理,大量減少中斷數量,降低系統消耗。這些調整的成本很低,難點在於對技術人員要求很高,需要對整個系統非常熟。

(三)編譯器級

還有編譯器級調優,需要有代碼,但是不修改代碼,使用編譯器的優化選項,有的時候也能夠獲得巨大的性能提高。比如引入自動向量化,深度優化,性能剖析指導的優化(PGO)等等。需要技術人員熟悉編譯的使用,以及對優化過程的理解。

(四)代碼級

還有代碼級調優,也就是直接改動實現代碼,改代碼收益或許非常大,比如換個演算法,因為有的演算法就是比別的演算法快很多倍,甚至快幾個數量級,但是修改的難度極其大,成本極高,代碼改了以後正確性要重新驗證,所有測試的步驟都要做。

在原有串列單執行緒程式中,如果有比較明顯的計算密集型迴圈,可以引入OpenMP進行並行化,結合編譯器的自動向量化編譯選項,可以只改極小一部分代碼,獲得比較大的性能收益。

黃新平先生緊接著介紹了在性能調優中常用的一些關鍵指標:系統性能指標包括CPU利用率、記憶體使用率,swap分區、頻寬使用率;處理器性能指標包括CPI、浮點運算的峰值、向量化比例、cache miss、DTLB miss等。

第一個,CPU利用率,CPU利用率其實內部包括了幾個方面,光看利用率數值是不夠的,比如CPU利用率100%,但是system time占了30%,這樣其實性能並不好,user time是CPU跑使用者程式的時間,越高越好,而system time 是消耗在內核裡面的時間,通常是由網路、磁片的一些中斷導致的,更常見的是鎖。還有一個是io wait,當大量磁片讀取的時候就會有比較明顯的占比。

還有一個是swap的使用, 當使用到swap的時候會感覺很慢,幾乎沒有回應了,這是為什麼?是因為硬碟的訪問及其緩慢。 大家通常對特別小的資料沒有太多概念。一般來講,CPU取第一級Cache典型的時間是十來個時鐘週期,如果按2G赫茲算的話是幾個納秒,而硬碟訪問的延遲大概是幾十個毫秒。有一個很形象的比喻,如果從一級緩存取數需要1秒的時間的話,那麼從硬碟中讀取資料就需要116天。

另外一個是網路,網路分為千兆網、萬兆網、infiniband網路,這個千兆網就是1000比特每秒,萬兆就是10000比特每秒。infiniband是一個非常快速的網路,它能達到40G到100G。

到了CPU裡面,有一個重要的指標叫CPI(Cycles Pre Instruction),就是每條指令需要多少個時鐘週期完成,現在主流處理器的理論值是0.25,因為它有4個算術邏輯運算單元。實際上測下來,比如Linpack能夠達到0.33。為什麼CPI會升高,因為指令有記憶體訪問的時候它不得不等待,雖然亂序執行能夠掩蓋一部分延遲,但是不可能完全填補運算速度與記憶體訪問直接的差距,這個指標值就上來了。

另外一個指標是浮點運算的峰值,如果大家關心過GPU的一般都會知道這個數,每秒鐘執行多少浮點型運算。實際運行的浮點型運算的性能值和理論峰值之間的比值就是浮點運算單元的使用率,很不幸的是並行科技運維過很多計算中心,發現絕大多數應用的這個使用率小於10%, 也就是說買了一個160公里時速的車永遠以10多公里的時速開, 問題基本都在軟體層面上。有時候優化過後,就可以達到60%到70%多,一下子性能提升了六七倍。

向量化比例是剛才浮點型運算效率偏低的原因,因為沒用向量化,還在用單個標量運算,肯定性能比較爛,利用向量化之後,一條指令執行16-32條資料就處理完了,而標量計算才處理一個,很多時候就是改一改編譯器的選項就完成了。

Cache miss比例是指數據訪問的時候沒有在快取記憶體裡找到的,需要到記憶體中去取,肯定會帶來較大的性能損失,這個miss比率對性能的影響比較大, 性能好的時候是很低的資料,一般要小於10%。

還有一個是DTLB miss。進程訪問的位址空間是邏輯位址空間,比如64位元的程式,位址範圍從0到2的64次方減一,然而這麼大的位址空間,實際上不可能配置這麼多實體記憶體的,需要建立一個從邏輯記憶體到實體記憶體的映射表,這個映射表的管理是按照頁為最小單位的,缺省的頁大小是4K。但是頁大小可調,越大的頁,頁表條目越少,頁大小為4M的跟4k的頁表條目就差了1024倍。這些頁表不可能完全放在CPU裡面,CPU只能放在一個稱為DTLB的緩衝區中,更多的部分放在記憶體裡面,當映射相應條目的時候,如果在緩衝區找不到就會產生DTLB miss的異常,這個異常處理會從記憶體中找到相關條目,然後放入DTLB緩衝區。這個操作極其耗時,經常發生時對性能影響極大,通常可以通過調整頁大小,或者在程式中使用記憶體池等記憶體管理技術減少耗時。

還有像記憶體頻寬,記憶體頻寬體現了記憶體訪問的忙閑程度,這個值高到一定程度,會導致記憶體延遲迅速增加,有一些工具比如並行的Paramon和Intel的VTune可以幫助測量這個值。

三、並行應用調優工具

工欲善其事必先利其器,工具按照剛才調優的級別會有不同,一個是系統級的分析,包括CPU、GPU、網路、記憶體、swap等。一是應用級分析,包括代碼中各函數的耗時、MPI計算與通信占比等。

並行科技的Paramon應用性能收集監控工具,會即時收集大量性能指標,即時顯示並分析常見性能問題,並將這些資料記錄下來,供Paratune應用性能分析工具進行詳細分析,Paratune可以即時重播集群各伺服器性能指標,並且將這一段的運行特徵分析出來,以四個象限展示,是CPU密集型、記憶體訪問密集型、磁片密集型還是網路密集型一目了然。這是並行最早的產品,現在這一塊作為HPC雲計算服務的一個基礎的工具存在,我們給客戶服務的時候全是拿這些東西做分析。建立了一個大量的運行特徵庫,可以直接指導應用的調優方向及匹配的硬體伺服器的選型與採購。

英特爾的工具VTune,這是一個調優神器,實際用起來操作很簡單,難在它給出了一大堆報告和資料之後怎麼樣解讀它,怎麼樣利用它。它可以迅速定位應用中最耗時的部分以及發現並行程式中不正確的同步導致的性能損失。直接對應到代碼幫助調優者迅速找到原因。

代碼級性能優化即代碼現代化的方法:

代碼級的性能優化,有一個非常好的,就是稱為代碼現代化的一系列方法。 最主要的有並行化,就是要多執行緒化,充分利用多核資源;另外一個是向量化,充分利用處理器向量位元寬,實現單指令多資料的處理;還有是記憶體訪問優化,在KNL或者GPU這樣的有高速高頻寬記憶體的時候,需要充分利用這些資源,將最常訪問的資料與代碼放入高頻寬記憶體區域,縮小處理器處理速度與記憶體存取速度之間的差距。

四、並行應用調優實戰

黃新平先生通過演示矩陣相乘程式闡述編譯參數的調整進行性能調優。

不改變代碼,僅利用編譯器參數進行調優。為我們演示了2048乘以2048的矩陣相乘的演算法。僅僅通過編譯器編譯選項的調整就可以大幅度提高運行效率。執行時間從25秒縮短到0.14秒,性能提高178倍,卻不需要改一行代碼。

黃新平先生通過Black Sholes演算法程式的調優過程演示了少量改變部分代碼的方式進行性能調優。

Black Sholes演算法分了兩個部分,第一個部分用亂數產生器產生大量亂數,模擬買賣單。 第二個部分利用類比資料計算當前的投資價值。首先用VTune尋找問題, 先找熱點,發現指數運算函數、對數運算函數加上一個亂數產生器函數被大量調用。而且它是一個單執行緒的程式,所以第一件事就是在類比計算部分的計算密集的for迴圈處加了OpenMP編譯指令,同時使用編譯器的自動向量化編譯選項,獲得了4倍的性能提升。繼續考察程式,發現初始化部分的亂數產生器, 在英特爾的MKL庫裡有一個非常好的實現,因此可以直接換上這個實現,最終總體程式獲得了22.8倍的性能提升。

演示中跑50萬個 模擬,三次反覆運算。基準性能資料是平均1472個時鐘週期做一個類比,初始化用了1073個時鐘週期,類比計算花了399個時鐘週期。使用OpenMP和向量化指令優化後, 總時間變成了364個時鐘週期,初始化用了338個,計算用了26個。換成MKL庫的亂數產生函數後,總時間變成了64, 初始化用了35,計算用了29。在一個雙核的筆記本上,計算時間從1472變成64,22.8倍的提升,實際代碼量改動非常小。如果在更多核的伺服器上將會有更大的性能提升。

問答精選

Q:Paramon性能採集工具是怎樣採集CPU以外的性能資料的,採集程式是否影響結果的準確性?

A:Paramon性能採集工具不需要修改運行程式,它對運行指標的收集都是從內核的資料結構裡面提取的,不會影響被監測程式實際的運行,額外負載非常低,通常單核在0.5%以下。

Q:您一直講得是比較靠近底層的,我們現在做人工智慧、深度學習,很多人並不會直接學習底層,如python語言,您今天講得調優有什麼幫助?

A:

Q:您今天講的平行計算裡面的一些技術方法主要以CPU為主,GPU可不可以運用?

A:可以的。平行計算在方法論上沒有任何區別, 但是GPU有自己的特點,需要針對這些特點做相應調整,比如GPU有更大規模的硬體執行緒,在使用上需要更好地劃分並行任務和並行資料集,以充分並行化利用硬體資源,在寫GPU程式的時候,非常常見的並行程式設計模式是流水線模式,也可以考慮使用函數式程式設計的思想,函數程式設計很容易實現並行。另外GPU相對CPU來講記憶體的容量較小,PCIe資料傳輸的速度很慢,要考慮好資料的傳輸問題。