by Dave Cheney
概觀
本次研讨會的目标是為您提供診斷Go應用程式中的性能問題并進行修複所需的工具。
通過這一天,我們将從小工作 - 學習如何編寫基準,然後分析一小段代碼。然後走出去讨論執行跟蹤器,垃圾收集器和跟蹤運作的應用程式。剩下的時間将是您提出問題的機會,并嘗試使用您自己的代碼。
您可以在此處找到此示範文稿的最新版本 http://bit.ly/dotgo2019 |
歡迎
你好,歡迎光臨!🎉
本次研讨會的目标是為您提供診斷Go應用程式中的性能問題并進行修複所需的工具。
通過這一天,我們将從小工作 - 學習如何編寫基準,然後分析一小段代碼。然後走出去讨論執行跟蹤器,垃圾收集器和跟蹤運作的應用程式。剩下的時間将是您提出問題的機會,并嘗試使用您自己的代碼。
教師
- 戴夫·切尼[email protected]
許可證和材料
本次研讨會是David Cheney與Francesc Campoy的合作。
本示範文稿根據知識共享署名 - 相同方式共享4.0國際許可進行許可。
先決條件
這是您今天需要的幾個軟體下載下傳。
研讨會資料庫
将源代碼下載下傳到本文檔并在https://github.com/davecheney/high-performance-go-workshop上編寫代碼示例
筆記本電腦,電源等
研讨會的材料針對Go 1.12。
下載下傳Go 1.12
如果你已經更新到Go 1.13就可以了。在較小的Go版本之間,優化選擇總會有一些小的變化,我會嘗試在我們進行時指出這些。 |
的Graphviz
關于pprof的部分要求
dot
程式随
graphviz
工具套件一起提供。
- Linux的:
[sudo] apt-get install graphviz
- OSX:
- MacPorts的:
sudo port install graphviz
- macx:
brew install graphviz
- Windows(未經測試)
谷歌浏覽器
執行跟蹤器的部分需要Google Chrome。它不适用于Safari,Edge,Firefox或IE 4.01。請告訴你的電池我很抱歉。
下載下傳谷歌浏覽器
您自己的代碼來分析和優化
當天的最後一部分将是一個開放式會議,您可以在其中試驗您學到的工具。
還有一件事......
這不是講座,而是談話。我們會有很多休息時間提問。
如果您不了解某些内容,或者認為您聽到的内容不正确,請詢問。
1.微處理器性能的過去,現在和未來
這是一個關于編寫高性能代碼的研讨會。在其他研讨會上,我談到了解耦設計和可維護性,但我們今天在這裡談論性能。
我想今天開始簡短的講座,講述我如何看待計算機發展的曆史,以及為什麼我認為編寫高性能軟體很重要。
現實情況是軟體在硬體上運作,是以談到編寫高性能代碼,首先我們需要讨論運作代碼的硬體。
1.1。了解機械
目前有一個流行的術語,你會聽到Martin Thompson或Bill Kennedy等人談論“機械上的了解”。
“機械了解”這個名字來自偉大的賽車手傑基斯圖爾特,他是世界一級方程式賽車冠軍的3倍。他相信最好的車手對機器如何工作有足夠的了解,是以他們可以與之協調工作。
要成為一名優秀的賽車手,你不需要成為一名出色的機械師,但你需要對馬車的工作方式有一個粗略的了解。
我相信我們作為軟體工程師也是如此。我認為這個會議室裡的任何人都不會成為專業的CPU設計師,但這并不意味着我們可以忽略CPU設計人員面臨的問題。
1.2。六個數量級
有一個常見的網際網路模因,就像這樣;
當然這是荒謬的,但它強調了計算行業的變化。
作為軟體作者,我們這個房間裡的所有人都受益于摩爾定律,40年來,晶片每18個月可用半導體數量翻了一番。沒有其他行業在一生的空間中經曆了六個數量級的工具改進[ 1 ]。
但這一切都在改變。
1.3。電腦還在變快嗎?
是以,基本問題是,面對上圖中的統計資料,我們應該問的問題是計算機是否仍然變得更快?
如果計算機仍然變得越來越快,那麼我們可能不需要關心代碼的性能,我們隻需稍等一下,硬體制造商将為我們解決性能問題。
1.3.1。我們來看看資料
這是您在教科書中找到的經典資料,如計算機體系結構, John L. Hennessy和David A. Patterson的定量方法。該圖取自第5版
在第5版中,Hennessey和Patterson認為計算性能有三個時代
- 第一個是1970年代和80年代初,這是形成時期。我們今天所知的微處理器并不存在,計算機是用分立半導體或小規模內建電路建構的。成本,規模和對材料科學了解的限制是限制因素。
- 從80年代中期到2004年,趨勢線很明顯。計算機整數性能平均每年提高52%。計算機功率每兩年翻一番,是以人們将摩爾定律與計算機性能相加,即模具上半導體數量增加一倍。
- 然後我們來到計算機性能的第三個時代。改進變慢了。總變化率為每年22%。
之前的圖表僅上升到2012年,但幸運的是在2012年,Jeff Preshing編寫了一個工具來抓取Spec網站并建構自己的圖表。
是以這是使用1995年至2017年的Spec資料的相同圖表。
對我來說,不是我們在2012年的資料中看到的階段變化,而是說單核心性能接近極限。對于浮點數而言,這些數字略好一些,但對于我們在會議室中進行業務線應用程式而言,這可能并不相關。
1.3.2。是的,電腦仍然變得越來越快
關于摩爾定律結束的第一件事就是戈登摩爾告訴我的事情。他說“所有指數都結束了”。- 約翰軒尼詩
這是軒尼詩引用Google Next 18和他的圖靈獎演講。他的論點是肯定的,CPU性能仍在提高。但是,單線程整數性能仍在每年提高2-3%左右。按此速度,它将需要20年的複合增長才能達到整數表現。相比之下,90年代的表現每兩年增加一倍。
為什麼會這樣?
1.4。時脈速度
2015年的圖表很好地證明了這一點。頂行顯示了晶片上的半導體數量。自1970年代以來,這一趨勢在一個大緻線性的趨勢線上繼續。由于這是log / lin圖,是以該線性系列代表指數增長。
然而,如果我們看一下中間線,我們看到時脈速度在十年内沒有增加,我們看到cpu速度在2004年左右停滞不前
下圖顯示了散熱功率; 即電能變成熱量,遵循相同的模式 - 時脈速度和cpu散熱是相關的。
1.5。熱量
為什麼CPU産生熱量?它是一個固态裝置,沒有移動元件,是以摩擦等效果在這裡并沒有(直接)相關。
該資料圖取自TI生産的優秀資料表。在該模型中,N型器件中的開關被吸引到正電壓P型器件被正電壓排斥。
CMOS器件的功耗,就是這個房間裡的每個半導體,桌面和口袋裡的三個因素的組合。
- 靜電。當半導體靜止時,即不改變其狀态時,有少量電流通過半導體洩漏到地。半導體越小,洩漏越多。洩漏随溫度升高而增加。當你擁有數十億個半導體時,即使是少量的洩漏也會增加!
- 動力。當半導體從一種狀态轉換到另一種狀态時,它必須對連接配接到栅極的各種電容充電或放電。每個半導體的動态功率是電容的平方乘以電容和變化的頻率。降低電壓可以降低半導體消耗的功率,但是較低的電壓會導緻半導體切換較慢。
- 撬棍或短路電流。我們喜歡将半導體視為數字裝置占據一個或另一個狀态,原子地關閉或打開。實際上,半導體是模拟器件。作為開關,半導體大部分開始關斷,并且轉換或切換到大部分開啟的狀态。這種轉換或切換時間非常快,在現代處理器中它的速度為皮秒,但仍然代表從Vcc到地的低電阻路徑的一段時間。半導體開關越快,其頻率越高,散熱量就越大。
1.6。Dennard縮放的結束
為了了解接下來發生的事情,我們需要檢視1974年由Robert H. Dennard共同撰寫的論文。Dennard的Scaling定律大緻指出随着半導體變小,它們的功率密度保持不變。較小的半導體可以在較低的電壓下運作,具有較低的栅極電容,并且開關速度更快,這有助于減少動态功率。
那怎麼辦呢?
結果并不那麼好。随着半導體的栅極長度接近幾個矽原子的寬度,半導體尺寸,電壓和重要的洩漏之間的關系被破壞。
在1999年的Micro-32會議上假設,如果我們遵循時脈速度增加和半導體尺寸縮小的趨勢線,那麼在處理器生成中,半導體結将接近核反應堆核心的溫度。顯然這是瘋狂的。奔騰4 标志着單核,高頻,消費類CPU 的終結。
回到這個圖表,我們看到時脈速度停滞的原因是因為cpu超出了我們冷卻它們的能力。到2006年,減小半導體的尺寸不再提高其功率效率。
我們現在知道降低CPU特征尺寸主要是為了降低功耗。降低能耗并不僅僅意味着“綠色”,就像回收一樣,拯救地球。主要目标是将功耗和熱耗散保持在低于損壞CPU的水準。
但是,圖表的一部分仍在繼續增加,即晶片上的半導體數量。cpu的行進特征是在相同的給定區域中具有更大的半導體,具有正面和負面效果。
此外,正如您在插頁中看到的那樣,每個半導體的成本持續下降,直到大約5年前,然後每個半導體的成本開始再次回升。
建立更小的半導體不僅成本越來越高,而且越來越難。2016年的這份報告顯示了2013年晶片制造商認為會發生什麼的預測; 兩年後,他們錯過了所有的預測,雖然我沒有這份報告的更新版本,但沒有迹象表明他們能夠扭轉這種趨勢。
英特爾,台積電,AMD和三星花費數十億美元,因為他們必須建立新的晶圓廠,購買所有新的工藝工具。是以,雖然每個晶片的半導體數量持續增加,但其機關成本已開始增加。
甚至術語門長度(以納米為機關)也變得模棱兩可。各種制造商以不同的方式測量半導體的尺寸,使其能夠展示比競争對手更小的數量,而無需提供。這是CPU制造商的非GAAP收益報告模型。 |
1.7。更多核心(more cores)
由于達到了熱量和頻率限制,是以不再能夠使單核運作速度提高兩倍。但是,如果添加其他核心,則可以提供兩倍的處理能力 - 如果軟體可以支援它。
實際上,CPU的核心數量主要是散熱。Dennard縮放的結束意味着CPU的時脈速度是1到4 Ghz之間的任意數字,具體取決于它的熱度。當我們談論基準測試時,我們會很快看到這一點。
1.8。阿姆達爾定律
CPU不會變得越來越快,但随着超線程和多核的發展,它們的範圍越來越廣。移動部件上的雙核,桌面部件上的四核,伺服器部件上的數十個核心。這将成為計算機性能的未來嗎?不幸的是。
Amdahl定律以IBM / 360的設計者Gene Amdahl命名,是一個公式,它給出了在固定工作負載下執行任務的延遲的理論加速,這可以預期資源得到改善的系統。
Amdahl定律告訴我們,程式的最大加速時間受程式的連續部分的限制。如果編寫一個程式,其95%的執行能夠并行運作,即使有數千個處理器,程式執行的最大加速也限制為20倍。
想想你每天工作的程式,他們的執行程式有多少是可以分開的?
1.9。動态優化
随着時脈速度的停滞以及在問題上抛出額外核心的回報有限,加速從何而來?它們來自晶片本身的架構改進。這些是具有Nehalem,Sandy Bridge和Skylake等名稱的五到七年大型項目。
在過去二十年中,性能的大部分提升來自于體系結構的改進:
1.9.1。亂序執行
亂序,也稱為超标量,執行是一種從CPU執行的代碼中提取所謂的指令級并行性的方法。現代CPU在硬體級别有效地執行SSA以識别操作之間的資料依賴性,并且在可能的情況下并行地運作獨立指令。
但是,任何一段代碼中固有的并行數量都是有限的。它也非常耗電。大多數現代CPU已經确定每個核心有六個執行單元,因為在管道的每個階段都有一個n平方成本将每個執行單元連接配接到所有其他執行單元。
1.9.2。投機執行
儲存最小的微控制器,所有CPU利用指令流水線重疊指令擷取/解碼/執行/送出周期中的部分。
指令流水線的問題是分支指令,平均每5-8條指令發生一次。當CPU到達分支時,它無法檢視分支以外的其他指令來執行,并且它無法開始填充其管道,直到它知道程式計數器也将分支到何處。推測執行允許CPU“猜測” 分支指令仍在處理時分支将采用哪條路徑!
如果CPU正确預測分支,那麼它可以保持其指令管道滿。如果CPU無法預測正确的分支,那麼當它意識到錯誤時,它必須復原對其架構狀态所做的任何更改。由于我們都在學習Spectre風格的漏洞,有時這種復原并不像希望的那樣無縫。
當分支預測率低時,推測執行可能非常耗電。如果分支是錯誤預測的,那麼CPU不僅必須回溯到錯誤預測的點,而且浪費在錯誤分支上的能量。
所有這些優化都導緻了我們所見的單線程性能的提高,代價是大量的半導體和功率。
Cliff Click有一個精彩的示範文稿,它不按順序進行,而且推測性執行對于盡早啟動緩存未命中非常有用,進而減少了觀察到的緩存延遲。 |
1.10。現代CPU針對批量操作進行了優化
現代處理器就像硝基燃料的有趣汽車,它們在四分之一英裡表現出色。不幸的是,現代程式設計語言就像蒙特卡羅,它們充滿了曲折。 - 大衛Ungar
引自David Ungar,一位有影響力的計算機科學家和SELF程式設計語言的開發人員,我在網上找到了一個非常古老的示範文稿。
是以,現代CPU針對批量傳輸和批量操作進行了優化。在每個級别,操作的設定都會鼓勵您批量工作。一些例子包括
- 記憶體不是每個位元組加載,而是每多個緩存行加載,這就是為什麼對齊變得比以前的計算機更少的問題。
- 像MMX和SSE這樣的向量指令允許單個指令同時針對多個資料項執行,前提是您的程式可以以該形式表示。
1.11。現代處理器受記憶體延遲而非記憶體容量的限制
如果CPU的情況不夠糟糕,那麼來自房子記憶體方面的消息就不會好多了。
連接配接到伺服器的實體記憶體幾何增加。我在1980年代的第一台計算機有千位元組的記憶體。當我上高中的時候,我寫的所有論文都是386,有1.8兆位元組的公羊。現在,它常常找到具有數十或數百GB RAM的伺服器,而雲提供商正在推動數TB的記憶體。
但是,處理器速度和記憶體通路時間之間的差距仍在繼續增長。
但是,就等待記憶體而丢失的處理器周期而言,實體記憶體仍然遙不可及,因為記憶體跟不上CPU速度的增長。
是以,大多數現代處理器都受到記憶體延遲而非容量的限制。
1.12。緩存規則我周圍的一切
幾十年來,處理器/記憶體上限的解決方案是添加一個緩存 - 一塊靠近CPU的小型快速記憶體,現在直接內建到CPU上。
但;
- 幾十年來,L1一直停留在每核心32kb
- L2在最大的英特爾部分上緩慢爬升至512kb
- L3現在在4-32mb範圍内測量,但其通路時間是可變的
緩存的大小有限,因為它們在CPU裸片上體積很大,消耗大量功率。要将緩存未命中率減半,必須将緩存大小增加四倍。
1.13。免費午餐結束了
2005年,C ++委員會上司人Herb Sutter撰寫了一篇題為“免費午餐結束”的文章。在他的文章中,Sutter讨論了我所涵蓋的所有要點,并斷言未來的程式員将不再能夠依賴更快的硬體來修複慢速程式或減慢程式設計語言。
現在,十多年後,毫無疑問Herb Sutter是對的。記憶體很慢,緩存太小,CPU時脈速度倒退,而單線程CPU的簡單世界早已不複存在。
摩爾定律仍然有效,但對于我們這個房間裡的所有人來說,免費午餐已經結束了。
1.14。結論
我要引用的數字将是2010年:30GHz,100億個半導體和每秒1個tera指令。- 英特爾首席技術官Pat Gelsinger,2002年4月
很明顯,如果沒有材料科學的突破,那麼回歸到CPU性能同比增長52%的日子的可能性就會非常小。共同的共識是,錯誤不在于材料科學本身,而在于如何使用半導體。以矽表示的順序指令流的邏輯模型導緻了這種昂貴的終結。
網上有很多示範文稿重申了這一點。他們都有相同的預測 - 未來的計算機将不會像今天這樣程式設計。一些人認為它看起來更像是具有數百個非常愚蠢,非常不連貫的處理器的顯示卡。其他人認為,超長指令字(VLIW)計算機将成為主流。所有人都同意我們目前的順序程式設計語言與這些類型的處理器不相容。
我認為這些預測是正确的,硬體制造商在這一點上拯救我們的前景是嚴峻的。但是,今天我們為今天的硬體編寫的程式有很大的優化空間。Rick Hudson在GopherCon 2015上發表了關于重新使用軟體的“良性循環”的說法,該軟體與我們今天的硬體配合使用,而不是它的不一緻。
看看我之前展示的圖表,從2015年到2018年,整數性能提升了5-8%,而且記憶體延遲時間更少,Go團隊将垃圾收集器暫停時間減少了兩個數量級。Go 1.11程式顯示出比使用Go 1.6在相同硬體上的相同程式明顯更好的GC延遲。這些都不是來自硬體。
是以,為了在當今世界的當今硬體上獲得最佳性能,您需要一種程式設計語言:
- 是編譯的,而不是解釋的,因為解釋的程式設計語言與CPU分支預測器和推測執行的互動性很差。
- 您需要一種允許編寫高效代碼的語言,它需要能夠有效地讨論位和位元組以及整數的長度,而不是假裝每個數字都是理想的浮點數。
- 你需要一種語言讓程式員有效地讨論記憶體,思考結構與java對象,因為所有指針追逐都會給CPU緩存帶來壓力,而緩存未命中會燒掉數百個周期。
- 作為應用程式性能而擴充到多個核心的程式設計語言取決于它使用其緩存的效率以及它在多個核心上并行工作的效率。
顯然我們在這裡談論Go,我相信Go繼承了我剛才描述的許多特征。
1.14.1。這對我們意味着什麼?
隻有三個優化:少做。少做一些。做得更快。
最大的收益來自1,但我們将所有時間都花在了3上。 - Michael Fromberger
本講座的目的是說明當你談論程式或系統的性能完全在軟體中時。等待更快的硬體來挽救這一天是一個愚蠢的錯誤。
但有一個好消息,我們可以在軟體方面做出一些改進,這就是我們今天要讨論的内容。
1.14.2。進一步閱讀
- 微處理器的未來,Sophie Wilson JuliaCon 2018
- 50年的計算機架構:從大型機CPU到DNN TPU,David Patterson
- 計算的未來,約翰軒尼詩
- 計算的未來:與John Hennessy的對話 (Google I / O '18)
2.基準測試
測量兩次并切一次。 - 古老的諺語
在我們嘗試提高一段代碼的性能之前,首先我們必須知道它目前的性能。
本節重點介紹如何使用Go測試架構建構有用的基準測試,并提供避免陷阱的實用技巧。
2.1。基準規則基準
在進行基準測試之前,您必須擁有穩定的環境才能獲得可重複的結果。
- 機器必須處于空閑狀态 - 不要在共享硬體上進行配置,不要在等待長基準運作時浏覽網頁。
- 注意省電和熱縮放。這些在現代筆記本電腦上幾乎是不可避免的。
- 避免虛拟機和共享雲托管; 對于一緻的測量,它們可能太嘈雜。
如果您負擔得起,請購買專用的性能測試硬體。機架,禁用所有電源管理和熱縮放,永不更新這些機器上的軟體。從系統管理的角度來看,最後一點是糟糕的建議,但如果軟體更新改變了核心或庫執行的方式 - 想想Spectre更新檔 - 這将使之前的任何基準測試結果無效。
對于我們其他人來說,有一個前後樣本并多次運作它們以獲得一緻的結果。
2.2。使用測試包進行基準測試
該
testing
軟體包内置支援編寫基準測試。如果我們有這樣一個簡單的函數:
func Fib(n int) int {
switch n {
case 0:
return 0
case 1:
return 1
case 2:
return 2
default:
return Fib(n-1) + Fib(n-2)
}
}
我們可以使用該
testing
包為該函數編寫函數的基準。
func BenchmarkFib20(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(20) // run the Fib function b.N times
}
}
基準函數與 檔案中的測試一起存在。 |
基準測試類似于測試,唯一真正的差別是他們需要的是一個
*testing.B
而不是一個
*testing.T
。這兩種類型的實作
testing.TB
提供類似的人群的最愛接口
Errorf()
,
Fatalf()
和
FailNow()
。
2.2.1。運作包的基準
基準測試使用
testing
它們通過
go test
子指令執行它們。但是,預設情況下,在您調用時
go test
,将排除基準。
要在包中顯式運作基準測試,請使用
-bench
标志。
-bench
采用與您要運作的基準測試名稱相比對的正規表達式,是以調用包中所有基準測試的最常用方法是
-bench=.
。這是一個例子:
% go test -bench=. ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib20-8 30000 40865 ns/op
PASS
ok _/Users/dfc/devel/high-performance-go-workshop/examples/fib 1.671s
也會在比對基準測試之前在包中運作所有測試,是以如果你在包中有很多測試,或者他們需要很長時間才能運作,你可以通過提供 一個沒有比對的正規表達式的标志來排除它們; 即。 |
2.2.2。基準測試的工作原理
每個基準函數都被調用不同的值
b.N
,這是基準應該運作的疊代次數。
b.N
從1開始,如果基準函數在1秒内完成 - 預設值 - 然後
b.N
增加,基準函數再次運作。
b.N
大緻順序增加; 1,2,3,5,10,20,30,50,100等。基準測試架構試圖變得聰明,如果它看到較小的值
b.N
相對較快地完成,它将更快地增加疊代次數。
看看上面的例子,
BenchmarkFib20-8
發現循環的大約30,000次疊代隻需要一秒鐘。從那裡開始,基準架構計算出每次操作的平均時間為40865ns。
所述 字尾涉及的值 被用來運作該測試。此數字 預設為啟動時Go程序可見的CPU數。您可以使用 标記更改此值,該标記采用值清單來運作基準測試。 這顯示了使用1,2和4核運作基準測試。在這種情況下,該标志對結果幾乎沒有影響,因為該基準是完全順序的。 |
2.2.3。提高基準精度
該
fib
函數是一個稍微有點人為的例子 - 除非您編寫TechPower Web伺服器基準測試 - 您的業務不太可能被計算在計算Fibonaci序列中第20個數字的速度。但是,基準測試确實提供了有效基準的忠實示例。
具體而言,您希望您的基準測試運作數萬次疊代,以便您獲得每次操作的良好平均值。如果您的基準測試僅運作100次或10次疊代,則這些運作的平均值可能具有較高的标準偏差。如果您的基準測試運作數百萬或數十億次疊代,平均值可能非常準确,但受到代碼布局和對齊的影響。
為了增加疊代次數,可以使用
-benchtime
标志增加基準時間。例如:
% go test -bench=. -benchtime=10s ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib20-8 300000 39318 ns/op
PASS
ok _/Users/dfc/devel/high-performance-go-workshop/examples/fib 20.066s
b.N
跑到相同的基準測試,直到它達到一個超過10秒的傳回值。當我們運作10倍以上時,疊代總數會增加10倍。結果沒有太大變化,這是我們的預期。
為什麼報告的總時間為20秒,而不是10秒?
如果你有一個運作毫安或數十億疊代的基準測試,導緻微操作或納秒範圍内的每個操作的時間,你可能會發現你的基準數字不穩定,因為熱縮放,記憶體局部性,背景處理,gc活動等。
對于每次操作10或單個數字納秒的時間,指令重新排序和代碼對齊的相對論效應将對您的基準時間産生影響。
要使用
-count
标志多次處理此運作基準測試:
% go test -bench=Fib1 -count=10 ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 1000000000 1.95 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 2000000000 1.97 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 2000000000 1.96 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 2000000000 2.01 ns/op
BenchmarkFib1-8 2000000000 1.99 ns/op
BenchmarkFib1-8 1000000000 2.00 ns/op
基準測試
Fib(1)
需要大約2納秒,方差為+/- 2%。
Go 1.12中的新
-benchtime
标志現在需要進行多次疊代,例如。
-benchtime=20x
這将完全運作您的代碼
benchtime
。
嘗試使用
-benchtime
10x,20x,50x,100x和300x 運作上面的fib台。你看到了什麼?
如果您發現 需要針對特定軟體包調整适用的預設值,我建議将這些設定編成一個, 以便每個想要運作基準測試的人都可以使用相同的設定進行編碼。 |
2.3。将基準與benchstat進行比較
在上一節中,我建議不止一次運作基準測試以獲得更多資料。由于我在本章開頭提到的電源管理,背景程序和熱管理的影響,這對任何基準測試都是很好的建議。
我将介紹Russ Cox的一個名為benchstat的工具。
% go get golang.org/x/perf/cmd/benchstat
Benchstat可以采取一系列基準測試,并告訴您它們的穩定性。這是
Fib(20)
關于電池電量的示例。
% go test -bench=Fib20 -count=10 ./examples/fib/ | tee old.txt
goos: darwin
goarch: amd64
BenchmarkFib20-8 50000 38479 ns/op
BenchmarkFib20-8 50000 38303 ns/op
BenchmarkFib20-8 50000 38130 ns/op
BenchmarkFib20-8 50000 38636 ns/op
BenchmarkFib20-8 50000 38784 ns/op
BenchmarkFib20-8 50000 38310 ns/op
BenchmarkFib20-8 50000 38156 ns/op
BenchmarkFib20-8 50000 38291 ns/op
BenchmarkFib20-8 50000 38075 ns/op
BenchmarkFib20-8 50000 38705 ns/op
PASS
ok _/Users/dfc/devel/high-performance-go-workshop/examples/fib 23.125s
% benchstat old.txt
name time/op
Fib20-8 38.4µs ± 1%
benchstat
告訴我們平均值為38.8微秒,樣本間的變化為+/- 2%。這對電池電量非常好。
- 第一次運作是最慢的,因為作業系統的CPU時鐘已經降低以節省功耗。
- 接下來的兩次運作是最快的,因為作業系統決定這不是一個短暫的工作峰值,它提高了時脈速度,以盡快通過工作,希望能夠傳回睡覺。
- 其餘的運作是用于産熱的作業系統和bios交易功耗。
2.3.1。提高Fib
确定兩組基準測試之間的性能差異可能是單調乏味且容易出錯的。Benchstat可以幫助我們解決這個問題。
儲存基準運作的輸出很有用,但您也可以儲存生成它的二進制檔案。這使您可以重新運作基準測試以前的疊代。為此,使用 标志來儲存測試二進制檔案 - 我經常将此二進制檔案重命名 為 。 |
先前的
Fib
功能具有斐波那契系列中第0和第1個數字的寫死值。之後,代碼以遞歸方式調用自身。我們将在今天晚些時候談論遞歸的成本,但目前,假設它有成本,特别是因為我們的算法使用指數時間。
簡單的解決方法就是從斐波納契系列中寫死另一個數字,将每個重複調用的深度減少一個。
func Fib(n int) int {
switch n {
case 0:
return 0
case 1:
return 1
case 2:
return 1
default:
return Fib(n-1) + Fib(n-2)
}
}
該檔案還包括一個全面的測試 。如果沒有驗證目前行為的測試,請不要嘗試改進基準測試。 |
為了比較我們的新版本,我們編譯了一個新的測試二進制檔案并對它們進行基準測試并用于
benchstat
比較輸出。
% go test -c
% ./fib.golden -test.bench=. -test.count=10 > old.txt
% ./fib.test -test.bench=. -test.count=10 > new.txt
% benchstat old.txt new.txt
name old time/op new time/op delta
Fib20-8 44.3µs ± 6% 25.6µs ± 2% -42.31% (p=0.000 n=10+10)
比較基準測試時要檢查三件事
- 新舊時代的方差±。1-2%是好的,3-5%是好的,大于5%并且您的一些樣品将被認為是不可靠的。在比較一方具有高差異的基準時要小心,您可能沒有看到改進。
- p值。p值低于0.05是好的,大于0.05意味着基準可能沒有統計學意義。
- 缺少樣品。benchstat将報告它認為有效的舊樣本和新樣本的數量,有時您可能隻會發現9個報告,即使您這樣做了
。10%或更低的拒絕率是可以的,高于10%可能表明您的設定不穩定,并且您可能比較的樣本太少。-count=10
2.4。避免基準測試啟動成本
有時您的基準測試每次運作設定成本為一次。
b.ResetTimer()
将用于忽略設定中産生的時間。
func BenchmarkExpensive(b *testing.B) {
boringAndExpensiveSetup()
b.ResetTimer() for n := 0; n < b.N; n++ {
// function under test
}
}
重置基準計時器 |
如果每次循環疊代都有一些昂貴的設定邏輯,請使用
b.StopTimer()
和
b.StartTimer()
暫停基準計時器。
func BenchmarkComplicated(b *testing.B) {
for n := 0; n < b.N; n++ {
b.StopTimer() complicatedSetup()
b.StartTimer() // function under test
}
}
暫停基準計時器 |
恢複計時器 |
2.5。基準配置設定
配置設定計數和大小與基準時間密切相關。您可以告訴
testing
架構記錄被測代碼所做的配置設定數量。
func BenchmarkRead(b *testing.B) {
b.ReportAllocs()
for n := 0; n < b.N; n++ {
// function under test
}
}
以下是使用
bufio
軟體包基準測試的示例。
% go test -run=^$ -bench=. bufio
goos: darwin
goarch: amd64
pkg: bufio
BenchmarkReaderCopyOptimal-8 20000000 103 ns/op
BenchmarkReaderCopyUnoptimal-8 10000000 159 ns/op
BenchmarkReaderCopyNoWriteTo-8 500000 3644 ns/op
BenchmarkReaderWriteToOptimal-8 5000000 344 ns/op
BenchmarkWriterCopyOptimal-8 20000000 98.6 ns/op
BenchmarkWriterCopyUnoptimal-8 10000000 131 ns/op
BenchmarkWriterCopyNoReadFrom-8 300000 3955 ns/op
BenchmarkReaderEmpty-8 2000000 789 ns/op 4224 B/op 3 allocs/op
BenchmarkWriterEmpty-8 2000000 683 ns/op 4096 B/op 1 allocs/op
BenchmarkWriterFlush-8 100000000 17.0 ns/op 0 B/op 0 allocs/op
您還可以使用該 标志強制測試架構報告所有運作的基準測試的配置設定統計資訊。 |
2.6。注意編譯器優化
這個例子來自問題14813。
const m1 = 0x5555555555555555
const m2 = 0x3333333333333333
const m4 = 0x0f0f0f0f0f0f0f0f
const h01 = 0x0101010101010101
func popcnt(x uint64) uint64 {
x -= (x >> 1) & m1
x = (x & m2) + ((x >> 2) & m2)
x = (x + (x >> 4)) & m4
return (x * h01) >> 56
}
func BenchmarkPopcnt(b *testing.B) {
for i := 0; i < b.N; i++ {
popcnt(uint64(i))
}
}
您認為此功能的基準測試速度有多快?我們來看看。
%go test -bench =。./examples/popcnt/
goos:達爾文
goarch:amd64
BenchmarkPopcnt-8 2000000000 0.30 ns / op
通過
0.3納秒; 這基本上是一個時鐘周期。即使假設CPU每個時鐘周期内可能有一些飛行指令,這個數字似乎也不合理地低。發生了什麼?
要了解發生了什麼,我們必須看看benchmake下的功能
popcnt
。
popcnt
是一個葉子函數 - 它不調用任何其他函數 - 是以編譯器可以内聯它。
因為函數是内聯的,是以編譯器現在可以看到它沒有副作用。
popcnt
不會影響任何全局變量的狀态。是以,呼叫被消除。這是編譯器看到的:
func BenchmarkPopcnt(b *testing.B) {
for i := 0; i < b.N; i++ {
// optimised away
}
}
在我測試過的所有Go編譯器版本中,仍然會生成循環。但是英特爾CPU非常擅長優化循環,尤其是空循環。
2.6.1。練習,看看大會
在我們繼續之前,讓我們看看元件以确認我們看到了什麼
% go test -gcflags=-S
使用`gcflags =“ - l -S”禁用内聯,這會如何影響程式集輸出
優化是一件好事 要帶走的是同樣的優化,通過删除不必要的計算,使實際代碼快速,與移除沒有可觀察到的副作用的基準相同的優化。 随着Go編譯器的改進,這隻會變得更加普遍。 |
2.6.2。修複基準
禁用内聯以使基準工作是不現實的; 我們希望通過優化來建構我們的代碼。
要修複此基準測試,我們必須確定編譯器無法證明主體
BenchmarkPopcnt
不會導緻全局狀态發生變化。
var Result uint64
func BenchmarkPopcnt(b *testing.B) {
var r uint64
for i := 0; i < b.N; i++ {
r = popcnt(uint64(i))
}
Result = r
}
這是確定編譯器無法優化循環體的推薦方法。
首先,我們通過存儲它來使用調用的結果。其次,因為一旦基準結束,就在本地範圍内聲明,結果永遠不會被程式的另一部分看到,是以作為最終行為,我們将值賦給包公共變量。
popcnt
r
r
BenchmarkPopcnt
r
r
Result
因為
Result
是公共的,編譯器無法證明導入這個的另一個包将無法看到
Result
随時間變化的值,是以它無法優化導緻其指派的任何操作。
如果我們
Result
直接配置設定會怎麼樣?這會影響基準時間嗎?那麼如果我們配置設定的結果
popcnt
來
_
?
在我們之前的 基準測試中,如果我們這樣做,我們沒有采取這些預防措施? |
2.7。基準錯誤
該
for
循環是基準的運作至關重要。
這是兩個不正确的基準,你能解釋一下它們有什麼問題嗎?
func BenchmarkFibWrong(b *testing.B) {
Fib(b.N)
}
func BenchmarkFibWrong2(b *testing.B) {
for n := 0; n < b.N; n++ {
Fib(n)
}
}
運作這些基準測試,您看到了什麼?
2.8。分析基準
該
testing
軟體包内置支援生成CPU,記憶體和塊配置檔案。
-
寫一個CPU配置檔案-cpuprofile=$FILE
。$FILE
-
,寫一個記憶體配置檔案-memprofile=$FILE
,$FILE
調整配置檔案率-memprofilerate=N
。1/N
-
,寫一個塊配置檔案-blockprofile=$FILE
。$FILE
使用這些标志中的任何一個也會保留二進制檔案。
% go test -run=XXX -bench=. -cpuprofile=c.p bytes
% go tool pprof c.p
2.9。讨論
有沒有問題?
也許現在是時候休息了。
3.績效衡量和分析
在上一節中,我們研究了各個函數的基準測試,當您提前知道瓶頸時,這些函數非常有用。但是,通常你會發現自己處于詢問的位置
為什麼這個程式運作這麼長時間?
分析整個程式,這對于回答諸如此類的進階問題非常有用。在本節中,我們将使用Go内置的分析工具從内部調查程式的操作。
3.1。pprof
我們今天要讨論的第一個工具是pprof。pprof來自Google Perf Tools工具套件,并且自最早的公開釋出以來已經內建到Go運作時中。
pprof
由兩部分組成:
-
每個Go程式都内置了一個包runtime/pprof
-
用于調查配置檔案。go tool pprof
3.2。配置檔案的類型
pprof支援幾種類型的分析,我們今天将讨論其中的三種:
- CPU分析。
- 記憶體分析。
- 阻止(或阻止)分析。
- Mutex争用分析。
3.2.1。CPU分析
CPU分析是最常見的配置檔案類型,也是最明顯的。
啟用CPU分析後,運作時将每隔10ms自行中斷并記錄目前運作的goroutine的堆棧跟蹤。
配置檔案完成後,我們可以對其進行分析以确定最熱門的代碼路徑。
函數在配置檔案中出現的次數越多,代碼路徑占總運作時間的百分比就越多。
3.2.2。記憶體分析
記憶體分析在進行堆配置設定時記錄堆棧跟蹤。
堆棧配置設定被認為是免費的,并not_tracked在存儲配置檔案。
記憶體分析,如CPU分析是基于樣本的,預設情況下每1000次配置設定中的記憶體分析樣本1。這個比率可以改變。
由于記憶體分析是基于樣本的,并且因為它跟蹤配置設定不使用,是以使用記憶體分析來确定應用程式的總記憶體使用量是很困難的。
個人意見:我發現記憶體分析對查找記憶體洩漏沒有用。有更好的方法可以确定應用程式使用的記憶體量。我們稍後将在示範文稿中讨論這些内容。
3.2.3。阻止分析
塊分析對于Go來說是非常獨特的。
塊配置檔案類似于CPU配置檔案,但它記錄了goroutine等待共享資源所花費的時間。
這對于确定應用程式中的并發瓶頸非常有用。
阻止分析可以顯示大量goroutine何時可以取得進展但被阻止。阻止包括:
- 在無緩沖的頻道上發送或接收。
- 發送到完整頻道,從空頻道接收。
- 嘗試
一個Lock
被另一個goroutine中鎖定。sync.Mutex
塊分析是一種非常專業的工具,在您認為已消除所有CPU和記憶體使用瓶頸之前,不應使用它。
3.2.4。互斥分析
Mutex分析類似于阻止分析,但專門針對導緻互斥争用導緻延遲的操作。
我對這種類型的配置檔案沒有很多經驗,但我已經建立了一個示例來示範它。我們很快就會看一下這個例子。
3.3。一個時間檔案
分析不是免費的。
分析對程式性能具有适度但可測量的影響 - 尤其是在增加記憶體配置檔案采樣率的情況下。
大多數工具不會阻止您一次啟用多個配置檔案。
不要一次啟用多種配置檔案。 如果您同時啟用多個配置檔案,他們将觀察自己的互動并抛棄您的結果。 |
3.4。收集個人資料
Go運作時的分析界面存在于
runtime/pprof
包中。
runtime/pprof
是一種非常低級别的工具,由于曆史原因,不同類型的配置檔案的接口不一緻。
正如我們在上一節中看到的那樣,pprof概要分析内置于
testing
包中,但有時在
testing.B
基準測試的上下文中放置您想要分析的代碼并且必須
runtime/pprof
直接使用API是不友善或困難的。
幾年前我寫了一個[小包] [0],以便更容易分析現有的應用程式。
import "github.com/pkg/profile"
func main() {
defer profile.Start().Stop()
// ...
}
我們将在本節中使用配置檔案包。當天晚些時候,我們将
runtime/pprof
直接使用界面。
3.5。使用pprof分析配置檔案
現在我們已經讨論了pprof可以測量的内容以及如何生成配置檔案,讓我們來談談如何使用pprof來分析配置檔案。
分析由
go pprof
子指令驅動
go tool pprof / path / to / your / profile
該工具提供了幾種不同的分析資料表示; 文本,圖形,甚至火焰圖。
如果你已經使用了Go一段時間,你可能會被告知 有兩個參數。從Go 1.9開始,配置檔案包含呈現配置檔案所需的所有資訊。您不再需要生成配置檔案的二進制檔案。🎉 |
3.5.1。進一步閱讀
- 分析Go程式(Go Blog)
- 調試Go程式中的性能問題
3.5.2。CPU分析(練習)
讓我們寫一個計算單詞的程式:
package main
import (
"fmt"
"io"
"log"
"os"
"unicode"
"github.com/pkg/profile"
)
func readbyte(r io.Reader) (rune, error) {
var buf [1]byte
_, err := r.Read(buf[:])
return rune(buf[0]), err
}
func main() {
defer profile.Start().Stop()
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf("could not open file %q: %v", os.Args[1], err)
}
words := 0
inword := false
for {
r, err := readbyte(f)
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("could not read file %q: %v", os.Args[1], err)
}
if unicode.IsSpace(r) && inword {
words++
inword = false
}
inword = unicode.IsLetter(r)
}
fmt.Printf("%q: %d words\n", os.Args[1], words)
}
讓我們來看看Herman Melville的經典Moby Dick中有多少單詞(來自Project Gutenberg)
% go build && time ./words moby.txt
"moby.txt": 181275 words
real 0m2.110s
user 0m1.264s
sys 0m0.944s
讓我們将它與unix進行比較
wc -w
% time wc -w moby.txt
215829 moby.txt
real 0m0.012s
user 0m0.009s
sys 0m0.002s
是以數字不一樣。
wc
因為它認為一個單詞與我的簡單程式所做的不同,是以大約高出19%。這并不重要 - 兩個程式都将整個檔案作為輸入,并在一次通過中計算從單詞到非單詞的轉換次數。
讓我們使用pprof調查這些程式為何具有不同的運作時間。
3.5.3。添加CPU分析
首先,編輯
main.go
并啟用分析
import (
"github.com/pkg/profile"
)
func main() {
defer profile.Start().Stop()
// ...
現在,當我們運作程式時,
cpu.pprof
會建立一個檔案。
% go run main.go moby.txt
2018/08/25 14:09:01 profile: cpu profiling enabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile239941020/cpu.pprof
"moby.txt": 181275 words
2018/08/25 14:09:03 profile: cpu profiling disabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile239941020/cpu.pprof
現在我們有了我們可以分析它的配置檔案
go tool pprof
% go tool pprof /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile239941020/cpu.pprof
Type: cpu
Time: Aug 25, 2018 at 2:09pm (AEST)
Duration: 2.05s, Total samples = 1.36s (66.29%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.42s, 100% of 1.42s total
flat flat% sum% cum cum%
1.41s 99.30% 99.30% 1.41s 99.30% syscall.Syscall
0.01s 0.7% 100% 1.42s 100% main.readbyte
0 0% 100% 1.41s 99.30% internal/poll.(*FD).Read
0 0% 100% 1.42s 100% main.main
0 0% 100% 1.41s 99.30% os.(*File).Read
0 0% 100% 1.41s 99.30% os.(*File).read
0 0% 100% 1.42s 100% runtime.main
0 0% 100% 1.41s 99.30% syscall.Read
0 0% 100% 1.41s 99.30% syscall.read
該
top
指令是您最常使用的指令。我們可以看到該計劃花費99%的時間
syscall.Syscall
,而且隻占一小部分
main.readbyte
。
我們還可以使用
web
指令可視化此調用。這将從配置檔案資料生成有向圖。在幕後,它使用
dot
Graphviz 的指令。
但是,在Go 1.10(可能是1.11)中,Go附帶了本機支援http伺服器的pprof版本
% go tool pprof -http=:8080 /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile239941020/cpu.pprof
将打開一個Web浏覽器;
- 圖形模式
- 火焰圖模式
在圖表中,消耗最多 CPU時間的框是最大的 - 我們看到
sys call.Syscall
在程式中花費的總時間的99.3%。導緻
syscall.Syscall
代表直接調用者的方框字元串- 如果多個代碼路徑聚合在同一個函數上,則可以有多個。箭頭的大小表示在一個盒子的子節點上花費了多少時間,我們看到它們從
main.readbyte
圖表的這個臂開始占據了1.41秒的近0。
問:有誰能猜到為什麼我們的版本比這麼慢
wc
?
3.5.4。改進我們的版本
我們的程式很慢的原因并不是因為Go的
syscall.Syscall
速度很慢。這是因為系統調用通常是昂貴的操作(并且随着發現更多的Spectre系列漏洞而變得越來越昂貴)。
每次調用都會
readbyte
産生一個緩沖區大小為1的syscall.Read。是以,我們程式執行的系統調用數等于輸入的大小。我們可以看到,在pprof圖中,讀取輸入主導其他所有内容。
func main() {
defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
// defer profile.Start(profile.MemProfile).Stop()
f, err := os.Open(os.Args[1])
if err != nil {
log.Fatalf("could not open file %q: %v", os.Args[1], err)
}
b := bufio.NewReader(f)
words := 0
inword := false
for {
r, err := readbyte(b)
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("could not read file %q: %v", os.Args[1], err)
}
if unicode.IsSpace(r) && inword {
words++
inword = false
}
inword = unicode.IsLetter(r)
}
fmt.Printf("%q: %d words\n", os.Args[1], words)
}
通過
bufio.Reader
在輸入檔案和
readbyte
将之間插入一個
比較修訂後的計劃的時間
wc
。它有多近?擷取個人資料,看看剩下的是什麼。
3.5.5。記憶體分析
新的
words
配置檔案表明在
readbyte
函數内部配置設定了一些東西。我們可以用pprof來調查。
defer profile.Start(profile.MemProfile).Stop()
然後像往常一樣運作程式
% go run main2.go moby.txt
2018/08/25 14:41:15 profile: memory profiling enabled (rate 4096), /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile312088211/mem.pprof
"moby.txt": 181275 words
2018/08/25 14:41:15 profile: memory profiling disabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile312088211/mem.pprof
Type: inuse_spaceTime: Mar 23, 2019 at 6:14pm (CET)Showing nodes accounting for 368.72kB, 100% of 368.72kB totalmainreadbyte368.72kB (100%)16B368.72kBruntimemain0 of 368.72kB (100%)mainmain0 of 368.72kB (100%)368.72kB368.72kB
因為我們懷疑配置設定來自
readbyte
- 這并不複雜,readbyte是三行長:
使用pprof确定配置設定的來源。
func readbyte(r io.Reader) (rune, error) {
var buf [1]byte _, err := r.Read(buf[:])
return rune(buf[0]), err
}
配置設定在這裡 |
我們将在下一節中詳細讨論為什麼會發生這種情況,但目前我們看到的是每次調用readbyte都會配置設定一個新的一個位元組長的數組,并且該數組正在堆上配置設定。
有什麼方法可以避免這種情況?嘗試使用它們并使用CPU和記憶體分析來證明它。
Alloc對象與使用對象
記憶體配置檔案有兩種,以其
go tool pprof
标志命名
-
報告每次配置設定的對象。-alloc_objects
-
如果在配置檔案末尾可以通路,則報告已進行配置設定的對象。-inuse_objects
為了證明這一點,這是一個人為的程式,它将以受控的方式配置設定一堆記憶體。
const count = 100000
var y []byte
func main() {
defer profile.Start(profile.MemProfile, profile.MemProfileRate(1)).Stop()
y = allocate()
runtime.GC()
}
// allocate allocates count byte slices and returns the first slice allocated.
func allocate() []byte {
var x [][]byte
for i := 0; i < count; i++ {
x = append(x, makeByteSlice())
}
return x[0]
}
// makeByteSlice returns a byte slice of a random length in the range [0, 16384).
func makeByteSlice() []byte {
return make([]byte, rand.Intn(2^14))
}
該程式是
profile
包的注釋,我們将記憶體配置檔案速率設定為
1
- 即,記錄每個配置設定的堆棧跟蹤。這會讓節目變得很慢,但是你會在一分鐘内看到原因。
% go run main.go
2018/08/25 15:22:05 profile: memory profiling enabled (rate 1), /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile730812803/mem.pprof
2018/08/25 15:22:05 profile: memory profiling disabled, /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile730812803/mem.pprof
讓我們看一下配置設定對象的圖形,這是預設設定,并顯示在配置檔案期間導緻配置設定每個對象的調用圖。
% go tool pprof -http=:8080 /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile891268605/mem.pprof
Type: alloc_objectsTime: Mar 23, 2019 at 1:08pm (GMT)Showing nodes accounting for 43837, 99.83% of 43910 totalDropped 66 nodes (cum <= 219)mainmakeByteSlice43806 (99.76%)16B43806runtimemain0 of 43856 (99.88%)mainmain0 of 43856 (99.88%)43856mainallocate31 (0.071%)of 43837 (99.83%)4380643837
毫不奇怪,超過99%的撥款都在内部
makeByteSlice
。現在讓我們使用相同的配置檔案
-inuse_objects
% go tool pprof -http=:8080 /var/folders/by/3gf34_z95zg05cyj744_vhx40000gn/T/profile891268605/mem.pprof
Type: inuse_objectsTime: Mar 23, 2019 at 1:08pm (GMT)Showing nodes accounting for 60, 100% of 60 totalruntimemalg24 (40.00%)384B24runtimeallocm7 (11.67%)of 21 (35.00%)71kB7runtimemcommoninit0 of 7 (11.67%)7runtimemstart0 of 17 (28.33%)runtimesystemstack3 (5.00%)of 14 (23.33%)14runtimemstart10 of 3 (5.00%)3runtimegcBgMarkWorker8 (13.33%)16B8runtimeschedule0 of 18 (30.00%)runtimeresetspinning0 of 15 (25.00%)15runtimestoplockedm0 of 3 (5.00%)3profileStartfunc82 (3.33%)of 9 (15.00%)16B196B1signalNotify3 (5.00%)of 7 (11.67%)7runtimemcall0 of 15 (25.00%)runtimepark_m0 of 15 (25.00%)1564B248B1runtimenewprocfunc10 of 11 (18.33%)11runtimenewm0 of 21 (35.00%)21runtimeensureSigMfunc10 of 5 (8.33%)runtimeLockOSThread0 of 3 (5.00%)3runtimechansend10 of 1 (1.67%)1runtimeselectgo0 of 1 (1.67%)1runtimestartm0 of 18 (30.00%)18144B116B148B1signalNotifyfunc10 of 4 (6.67%)4signalsignal_enable4 (6.67%)96B4runtimeacquireSudog2 (3.33%)96B2runtimemain0 of 6 (10.00%)mainmain0 of 6 (10.00%)6profileStart1 (1.67%)of 5 (8.33%)48B1profileStartfunc20 of 4 (6.67%)4log(*Logger)Output1 (1.67%)of 4 (6.67%)144B1log(*Logger)formatHeader0 of 3 (5.00%)3runtimenewproc10 of 11 (18.33%)10runtimeallgadd1 (1.67%)1timeLoadLocationFromTZData2 (3.33%)of 3 (5.00%)224B14kB1timebyteString1 (1.67%)15mainallocate0 of 1 (1.67%)1mainmakeByteSlice1 (1.67%)16B1128B116B1logPrintf0 of 4 (6.67%)4timeTimeDate0 of 3 (5.00%)341signalenableSignal0 of 4 (6.67%)44runtimestartTemplateThread0 of 3 (5.00%)3runtimechansend0 of 1 (1.67%)11runtimehandoffp0 of 3 (5.00%)3runtimempreinit0 of 7 (11.67%)7731115runtimewakep0 of 15 (25.00%)1513315sync(*Once)Do0 of 3 (5.00%)timeinitLocal0 of 3 (5.00%)3time(*Location)get0 of 3 (5.00%)3Timedate0 of 3 (5.00%)3Timeabs0 of 3 (5.00%)33timeloadLocation0 of 3 (5.00%)33
我們看到的不是在配置檔案期間配置設定的對象,而是在擷取配置檔案時仍在使用的對象- 這忽略了垃圾收集器已回收的對象的堆棧跟蹤。
3.5.6。阻塞分析
我們将看到的最後一個配置檔案類型是塊分析。我們将使用包中的
ClientServer
基準
net/http
% go test -run=XXX -bench=ClientServer$ -blockprofile=/tmp/block.p net/http
% go tool pprof -http=:8080 /tmp/block.p
Type: delayTime: Mar 23, 2019 at 6:05pm (CET)Showing nodes accounting for 7.82s, 100% of 7.82s totalDropped 39 nodes (cum <= 0.04s)runtimeselectgo4.55s (58.18%)testing(*B)runN0 of 5.06s (64.63%)http_testBenchmarkClientServer0 of 1.94s (24.83%)1.94stestingrunBenchmarksfunc10 of 3.11s (39.80%)3.11sruntimechanrecv13.23s (41.25%)runtimemain0 of 3.11s (39.80%)mainmain0 of 3.11s (39.80%)3.11shttp(*persistConn)writeLoop0 of 2.54s (32.44%)2.54stesting(*B)launch0 of 1.94s (24.82%)1.94sioutilReadAll0 of 0.11s (1.46%)0.11shttpGet0 of 1.83s (23.37%)1.83shttp(*persistConn)readLoop0 of 0.18s (2.36%)0.18ssync(*Cond)Wait0.04s (0.57%)http(*conn)serve0 of 0.04s (0.57%)http(*response)finishRequest0 of 0.04s (0.57%)0.04stesting(*B)Run0 of 3.11s (39.80%)testing(*B)run0 of 3.11s (39.77%)3.11shttp(*Transport)roundTrip0 of 1.83s (23.37%)http(*persistConn)roundTrip0 of 1.83s (23.36%)1.83sbytes(*Buffer)ReadFrom0 of 0.11s (1.46%)http(*bodyEOFSignal)Read0 of 0.11s (1.46%)0.11sioutilreadAll0 of 0.11s (1.46%)0.11s0.11shttp_testTestMain0 of 3.11s (39.80%)3.11shttp(*Client)Do0 of 1.83s (23.37%)http(*Client)do0 of 1.83s (23.37%)1.83shttp(*Client)Get0 of 1.83s (23.37%)1.83shttp(*Client)send0 of 1.83s (23.37%)1.83shttpsend0 of 1.83s (23.37%)1.83shttp(*Transport)RoundTrip0 of 1.83s (23.37%)1.83shttp(*bodyEOFSignal)condfn0 of 0.11s (1.46%)0.11shttp(*persistConn)readLoopfunc40 of 0.11s (1.46%)0.11shttp(*connReader)abortPendingRead0 of 0.04s (0.57%)0.04s0.11s1.83s0.04s1.83s1.83stesting(*M)Run0 of 3.11s (39.80%)3.11stesting(*B)doBench0 of 3.11s (39.77%)3.11stesting(*benchContext)processBench0 of 3.11s (39.77%)3.11stestingrunBenchmarks0 of 3.11s (39.80%)3.11s3.11s3.11s3.11s
3.5.7。線程建立分析
Go 1.11(?)添加了對分析作業系統線程建立的支援。
添加線程建立概要分析
godoc
并觀察概要分析的結果
godoc -http=:8080 -index
。
3.5.8。Framepointers
Go 1.7已經釋出,并且與amd64的新編譯器一起,編譯器現在預設啟用幀指針。
幀指針是一個始終指向目前堆棧幀頂部的寄存器。
Framepointers啟用類似工具
gdb(1)
,并
perf(1)
了解Go調用堆棧。
我們不會在本次研讨會中介紹這些工具,但您可以閱讀并觀看我用七種不同方式介紹Go程式的示範文稿。
- 描述Go程式的七種方法(幻燈片)
- 描述Go計劃的七種方式(視訊,30分鐘)
- 描述Go計劃的七種方式(網絡直播,60分鐘)
3.5.9。運作
- 從您熟悉的一段代碼生成配置檔案。如果您沒有代碼示例,請嘗試進行性能分析
。godoc
% go get golang.org/x/tools/cmd/godoc
% cd $GOPATH/src/golang.org/x/tools/cmd/godoc
% vim main.go
- 如果你要在一台機器上生成一個配置檔案并在另一台機器上檢查它,你會怎麼做?
4.編譯器優化
本節介紹Go編譯器執行的一些優化。
例如;
- 逃生分析
- 内聯
- 死代碼消除
都在編譯器的前端處理,而代碼仍然是AST形式; 然後将代碼傳遞給SSA編譯器以進行進一步優化。
4.1。Go編譯器的曆史
Go編譯器在2007年左右開始作為Plan9編譯器工具鍊的一個分支。當時的編譯器與Aho和Ullman的Dragon Book非常相似。
2015年,當時的Go 1.5編譯器從C機械翻譯成Go【又稱“自舉”】。
一年後,Go 1.7引入了一個基于SSA技術的新編譯器後端取代了之前的Plan 9樣式代碼生成。這個新的後端為泛型和體系結構特定的優化提供了許多機會。
4.2。逃逸分析
我們要讨論的第一個優化是逃逸分析(一種确定指針動态範圍的方法)。
為了說明逃逸分析确實回憶起Go規範沒有提到堆或堆棧。它隻提到語言在引言中是垃圾收集,并沒有提供如何實作這一點的提示。
Go規範的相容Go實作可以在堆上存儲每個配置設定。這會給垃圾收集器帶來很大的壓力,但這絕不是錯誤的 - 幾年來,gccgo對逃逸分析的支援非常有限,是以可以有效地被認為是在這種模式下運作。
但是,goroutine的堆棧作為存儲局部變量的廉價位置存在; 沒有必要在堆棧上垃圾收集。是以,在安全的情況下,放置在堆棧上的配置設定将更有效。
在某些語言中,例如C和C ++,在堆棧或堆上配置設定的選擇是程式員堆的配置設定的手動練習,
malloc
并且
free
堆棧配置設定是通過
alloca
。使用這些機制的錯誤是記憶體損壞錯誤的常見原因。
在Go中,如果值超出函數調用的生命周期,編譯器會自動将值移動到堆中。據說該值會 逃逸到堆中。
type Foo struct {
a, b, c, d int
}
func NewFoo() *Foo {
return &Foo{a: 3, b: 1, c: 4, d: 7}
}
在此示例中,已
Foo
配置設定的内容
NewFoo
将被移動到堆,是以其内容在
NewFoo
傳回後仍保持有效。
這一直存在于Go的早期。它不是一個優化的自動正确性功能。在Go中無法意外地傳回堆棧配置設定變量的位址。
但編譯器也可以做相反的事情; 它可以找到假定在堆上配置設定的東西,并将它們移動到堆棧。
我們來看一個例子吧
func Sum() int {
const count = 100
numbers := make([]int, count)
for i := range numbers {
numbers[i] = i + 1
}
var sum int
for _, i := range numbers {
sum += i
}
return sum
}
func main() {
answer := Sum()
fmt.Println(answer)
}
Sum
将`int`s添加到1到100之間并傳回結果。
因為
numbers
切片僅在内部引用
Sum
,是以編譯器将安排在堆棧上存儲該切片的100個整數,而不是堆。不需要垃圾回收
numbers
,
Sum
傳回時會自動釋放。
4.2.1。證明給我看!
要列印編譯器轉義分析決策,請使用該
-m
标志。
% go build -gcflags=-m examples/esc/sum.go
# command-line-arguments
examples/esc/sum.go:22:13: inlining call to fmt.Println
examples/esc/sum.go:8:17: Sum make([]int, count) does not escape
examples/esc/sum.go:22:13: answer escapes to heap
examples/esc/sum.go:22:13: io.Writer(os.Stdout) escapes to heap
examples/esc/sum.go:22:13: main []interface {} literal does not escape
<autogenerated>:1: os.(*File).close .this does not escape
第8行顯示編譯器已正确推斷出結果
make([]int, 100)
不會轉移到堆。之是以沒有
第22行報告
answer
逃逸到堆的原因
fmt.Println
是可變函數。到可變參數函數的參數被盒裝入一個切片,在這種情況下
[]interface{}
,使
answer
被放入一個接口值,因為它是由呼叫引用
fmt.Println
。由于圍棋1.6的垃圾收集器,需要所有通過接口被傳為指針,什麼編譯器看到的是價值約:
var answer = Sum()
fmt.Println([]interface{&answer}...)
我們可以使用
-gcflags="-m -m"
旗幟确認這一點。哪個回報
% go build -gcflags='-m -m' examples/esc/sum.go 2>&1 | grep sum.go:22
examples/esc/sum.go:22:13: inlining call to fmt.Println func(...interface {}) (int, error) { return fmt.Fprintln(io.Writer(os.Stdout), fmt.a...) }
examples/esc/sum.go:22:13: answer escapes to heap
examples/esc/sum.go:22:13: from ~arg0 (assign-pair) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13: io.Writer(os.Stdout) escapes to heap
examples/esc/sum.go:22:13: from io.Writer(os.Stdout) (passed to call[argument escapes]) at examples/esc/sum.go:22:13
examples/esc/sum.go:22:13: main []interface {} literal does not escape
總之,不要擔心第22行,它對這個讨論并不重要。
4.2.2。演習
- 這種優化是否适用于所有值
?count
- 如果
是變量而不是常數,這種優化是否成立?count
- 如果
是參數,這個優化是否成立count
?Sum
4.2.3。逃逸分析(續)
這個例子有點人為。它不是真正的代碼,隻是一個例子。
type Point struct{ X, Y int }
const Width = 640
const Height = 480
func Center(p *Point) {
p.X = Width / 2
p.Y = Height / 2
}
func NewPoint() {
p := new(Point)
Center(p)
fmt.Println(p.X, p.Y)
}
NewPoint
創造一個新的
*Point
價值
p
。我們傳遞
p
給将
Center
點移動到螢幕中心位置的功能。最後,我們列印的數值
p.X
和
p.Y
。
% go build -gcflags=-m examples/esc/center.go
# command-line-arguments
examples/esc/center.go:11:6: can inline Center
examples/esc/center.go:18:8: inlining call to Center
examples/esc/center.go:19:13: inlining call to fmt.Println
examples/esc/center.go:11:13: Center p does not escape
examples/esc/center.go:19:15: p.X escapes to heap
examples/esc/center.go:19:20: p.Y escapes to heap
examples/esc/center.go:19:13: io.Writer(os.Stdout) escapes to heap
examples/esc/center.go:17:10: NewPoint new(Point) does not escape
examples/esc/center.go:19:13: NewPoint []interface {} literal does not escape
<autogenerated>:1: os.(*File).close .this does not escape
即使
p
配置設定了該
new
函數,它也不會存儲在堆上,因為沒有引用會
p
轉義該
Center
函數。
問題:第19行怎麼樣,如果
p
沒有逃脫,什麼逃逸到堆?
寫一個基準來提供
Sum
不配置設定。
4.3。内聯
在Go函數中,調用具有固定的開銷; 堆棧和搶占檢查。
其中一些可以通過硬體分支預測器得到改善,但在功能大小和時鐘周期方面仍然是成本。
内聯是避免這些成本的經典優化。
直到Go 1.11内聯僅适用于葉子函數,一個不調用另一個函數的函數。對此的理由是:
- 如果你的功能做了很多工作,那麼前導碼開銷可以忽略不計。這就是為什麼函數超過一定的大小(目前有一些指令計數,加上一些阻止所有内聯在一起的操作(例如,在Go 1.7之前切換)
- 另一方面,小功能為相對少量的有用工作支付固定的開銷。這些是内聯目标的功能,因為它們受益最多。
另一個原因是重型内聯使得堆棧跟蹤更難以遵循。
4.3.1。内聯(示例)
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func F() {
const a, b = 100, 20
if Max(a, b) == b {
panic(b)
}
}
我們再次使用該
-gcflags=-m
标志來檢視編譯器優化決策。
% go build -gcflags=-m examples/inl/max.go
# command-line-arguments
examples/inl/max.go:4:6: can inline Max
examples/inl/max.go:11:6: can inline F
examples/inl/max.go:13:8: inlining call to Max
examples/inl/max.go:20:6: can inline main
examples/inl/max.go:21:3: inlining call to F
examples/inl/max.go:21:3: inlining call to Max
編譯器列印了兩行。
- 第3行的第一個,聲明
,告訴我們它可以内聯。Max
- 第二個是報告
第12行的内容已被内聯到呼叫者。Max
不使用
//go:noinline
注釋,重寫
Max
使得它仍然傳回正确的答案,但不再被編譯器認為是可内聯的。
4.3.2。内聯是什麼樣的?
編譯
max.go
并檢視優化版本的内容
F()
。
% go build -gcflags=-S examples/inl/max.go 2>&1 | grep -A5 '"".F STEXT'
"".F STEXT nosplit size=2 args=0x0 locals=0x0
0x0000 00000 (/Users/dfc/devel/high-performance-go-workshop/examples/inl/max.go:11) TEXT "".F(SB), NOSPLIT|ABIInternal, $0-0
0x0000 00000 (/Users/dfc/devel/high-performance-go-workshop/examples/inl/max.go:11) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (/Users/dfc/devel/high-performance-go-workshop/examples/inl/max.go:11) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (/Users/dfc/devel/high-performance-go-workshop/examples/inl/max.go:11) FUNCDATA $3, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x0000 00000 (/Users/dfc/devel/high-performance-go-workshop/examples/inl/max.go:13) PCDATA $2, $0
這是
F
曾經
Max
被内聯的主體- 這個功能沒有發生任何事情。我知道螢幕上有很多文字什麼都沒有,但是接受我的話,唯一發生的事情是
RET
。實際上
F
成了:
func F() {
return
}
什麼是FUNCDATA和PCDATA? 輸出 不是進入二進制檔案的最終機器代碼。連結器在最後的連結階段進行一些處理。類似 和 是垃圾收集器的中繼資料,在連結時移動到其他位置。如果你正在讀取輸出 ,隻需忽略 和 行; 它們不是最終二進制檔案的一部分。 |
4.3.3。讨論
為什麼我聲明
a
,并
b
在
F()
為常數?
嘗試輸出如果
a
和
b
被聲明為變量會發生什麼?如果
a
和作為參數
b
傳遞會發生什麼
F()
?
不會阻止在工作目錄中建構最終二進制檔案。如果發現後續運作 沒有産生輸出,請删除 工作目錄中的二進制檔案。 |
4.3.4。調整内聯級别
使用标志執行調整内聯級别
-gcflags=-l
。有些令人困惑的傳遞單個
-l
将禁用内聯,兩個或更多将啟用内聯更積極的設定。
-
,内聯禁用。-gcflags=-l
- 沒事,定期内聯。
-
内聯級别2,更具侵略性,可能更快,可能會制作更大的二進制檔案。-gcflags='-l -l'
-
内聯級别3,再次更具侵略性,二進制檔案肯定更大,可能更快,但也可能是錯誤的。-gcflags='-l -l -l'
-
Go 1.11中的四個“-l`s”将啟用實驗性中間堆棧内聯優化。-gcflags=-l=4
4.3.5。中間堆棧内聯
由于Go 1.12所謂的中間堆棧内聯已經啟用(之前在Go 1.11中預覽了
-gcflags='-l -l -l -l'
旗幟)。
我們可以在前面的示例中看到中間堆棧内聯的示例。在Go 1.11和更早版本中
F
,它不會是一個葉子函數 - 它會調用
max
。然而,由于内聯改進
F
現已内聯到其調用者中。這有兩個原因; 。當
max
内聯時
F
,不
F
包含其他函數調用,是以它成為潛在的葉函數,假設其複雜性預算未被超過。。因為
F
簡單的函數内聯和死代碼消除已經消除了它的大部分複雜性預算 - 無論調用如何,它都可以用于中間堆棧内聯
max
。
中間堆棧内聯可用于内聯函數的快速路徑,進而消除快速路徑中的函數調用開銷。 這個最近登陸的CL用于Go 1.13,顯示了這種技術适用于 。 |
進一步閱讀
- David Lazar在Go編譯器示範中的中間堆棧内聯
- 建議:Go編譯器中的中間堆棧内聯
4.4。死代碼消除
為什麼重要的是
a
和
b
是常數?
要了解發生了什麼事讓我們來看看編譯器看到一旦其内聯什麼
Max
成
F
。我們無法輕易地從編譯器中獲得這一點,但是它可以直接手動完成。
之前:
func Max(a, b int) int {
if a > b {
return a
}
return b
}
func F() {
const a, b = 100, 20
if Max(a, b) == b {
panic(b)
}
}
後:
func F() {
const a, b = 100, 20
var result int
if a > b {
result = a
} else {
result = b
}
if result == b {
panic(b)
}
}
因為
a
并且
b
是常量,編譯器可以在編譯時證明分支永遠不會為假;
100
永遠大于
20
。是以編譯器可以進一步優化
F
到
func F() {
const a, b = 100, 20
var result int
if true {
result = a
} else {
result = b
}
if result == b {
panic(b)
}
}
既然知道了分支的結果,那麼内容
result
也是已知的。這是呼叫分支消除。
func F() {
const a, b = 100, 20
const result = a
if result == b {
panic(b)
}
}
現在分支被消除,我們知道
result
總是等于
a
,因為
a
是一個常數,我們知道這
result
是一個常數。編譯器将此證明應用于第二個分支
func F() {
const a, b = 100, 20
const result = a
if false {
panic(b)
}
}
并且再次使用分支消除,最終形式
F
減少到。
func F() {
const a, b = 100, 20
const result = a
}
最後隻是
func F() {
}
4.4.1。死代碼消除(續)
分支消除是稱為死代碼消除的一類優化之一。實際上,使用靜态證明來表明一段代碼永遠不可達,通常稱為死,是以無需在最終二進制檔案中進行編譯,優化或發出。
我們看到了死代碼消除如何與内聯一起工作,以減少通過删除被證明無法通路的循環和分支生成的代碼量。
您可以利用此功能來實作昂貴的調試,并将其隐藏起來
const debug = false
結合建構标記,這可能非常有用。
4.4.2。進一步閱讀
- 使用// + build在調試和釋出版本之間切換
- 如何使用go build工具進行條件編譯
4.5。編譯器标志練習
編譯器标志提供:
go build -gcflags=$FLAGS
調查以下編譯器函數的操作:
-
列印正在編譯的包的(Go flavor)程式集。-S
-
控制内襯的行為; -l
禁用内聯,-l
增加它(更多-l -l
會增加編譯器對内聯代碼的興趣)。試驗編譯時間,程式大小和運作時間的差異。-l
-
控制優化決策的列印,如内聯,逃逸分析。-m
-m`列印出有關編譯器思考内容的更多細節。-m
-
禁用所有優化。-l -N
如果發現後續運作 沒有産生輸出,請删除 工作目錄中的二進制檔案。 |
4.5.1。進一步閱讀
- Jaana Burcu Dogan的Codegen檢查
4.6。界限檢查消除
Go是一種邊界檢查語言。這意味着檢查數組和切片下标操作以確定它們在相應類型的範圍内。
對于數組,這可以在編譯時完成。對于切片,這必須在運作時完成。
var v = make([]int, 9)
var A, B, C, D, E, F, G, H, I int
func BenchmarkBoundsCheckInOrder(b *testing.B) {
for n := 0; n < b.N; n++ {
A = v[0]
B = v[1]
C = v[2]
D = v[3]
E = v[4]
F = v[5]
G = v[6]
H = v[7]
I = v[8]
}
}
使用
-gcflags=-S
拆卸
BenchmarkBoundsCheckInOrder
。每個循環執行多少個邊界檢查操作?
func BenchmarkBoundsCheckOutOfOrder(b *testing.B) {
for n := 0; n < b.N; n++ {
I = v[8]
A = v[0]
B = v[1]
C = v[2]
D = v[3]
E = v[4]
F = v[5]
G = v[6]
H = v[7]
}
}
重新排列我們配置設定
A
直通的順序
I
會影響裝配。拆卸
BenchmarkBoundsCheckOutOfOrder
并找出答案。
4.6.1。演習
- 重新排列下标操作的順序是否會影響函數的大小?它會影響功能的速度嗎?
- 如果
移動到v
函數内部會發生什麼?Benchmark
- 如果
聲明為數組會發生什麼v
?var v [9]int
5.執行追蹤
執行追蹤器是由Dmitry Vyukov為Go 1.5 開發的,并且仍然記錄在案,并且未充分利用了好幾年。
與基于樣本的分析不同,執行跟蹤器內建到Go運作時,是以它隻知道Go程式在特定時間點正在做什麼,但為什麼。
5.1。什麼是執行跟蹤器,我們為什麼需要它?
我認為最容易解釋執行跟蹤器的作用,以及為什麼通過檢視pprof
go tool pprof
表現不佳的代碼片段來說這很重要。
該
examples/mandelbrot
目錄包含一個簡單的mandelbrot生成器。此代碼源自Francesc Campoy的mandelbrot包。
cd examples/mandelbrot
go build && ./mandelbrot
如果我們建構它,然後運作它,它會生成這樣的東西
5.1.1。多久時間?
那麼,該程式生成1024 x 1024像素圖像需要多長時間?
我知道如何做到這一點的最簡單方法是使用類似的東西
time(1)
。
% time ./mandelbrot
real 0m1.654s
user 0m1.630s
sys 0m0.015s
不要使用 或者你需要花費多長時間來編譯程式以及運作程式。 |
5.1.2。該計劃在做什麼?
是以,在這個例子中,程式用1.6秒生成mandelbrot并寫入png。
這樣好嗎?我們可以加快速度嗎?
回答這個問題的一種方法是使用Go的内置pprof支援來分析程式。
我們試試吧。
5.2。生成配置檔案
要生成配置檔案,我們需要
-
直接使用包。runtime/pprof
- 使用包裝器
來自動執行此操作。github.com/pkg/profile
5.3。使用runtime / pprof生成配置檔案
為了向您展示沒有魔力,讓我們修改程式以編寫CPU配置檔案
os.Stdout
。
import "runtime/pprof"
func main() {
pprof.StartCPUProfile(os.Stdout)
defer pprof.StopCPUProfile()
通過将此代碼添加到
main
函數的頂部,此程式将編寫配置檔案
os.Stdout
。
cd examples/mandelbrot-runtime-pprof
go run mandelbrot.go > cpu.pprof
我們可以 在這種情況下使用,因為cpu配置檔案隻包含執行 ,而不包括其編譯。 |
5.3.1。使用github.com/pkg/profile生成配置檔案
上一張幻燈片顯示了生成配置檔案的超級便宜方式,但它有一些問題。
- 如果您忘記将輸出重定向到檔案,那麼您将爆炸該終端會話。😞(提示:
是你的朋友)reset(1)
-
例如,如果你寫任何其他内容,os.Stdout
你将破壞跟蹤。fmt.Println
建議使用的方法
runtime/pprof
是将跟蹤寫入檔案。但是,你必須確定跟蹤停止,檔案在你的程式停止之前關閉,包括是否有人`^ C'。
是以,幾年前我寫了一個包來照顧它。
import "github.com/pkg/profile"
func main() {
defer profile.Start(profile.CPUProfile, profile.ProfilePath(".")).Stop()
如果我們運作此版本,我們會将配置檔案寫入目前工作目錄
% go run mandelbrot.go
2017/09/17 12:22:06 profile: cpu profiling enabled, cpu.pprof
2017/09/17 12:22:08 profile: cpu profiling disabled, cpu.pprof
使用 不是強制性的,但它會收集很多關于收集和記錄痕迹的樣闆,是以我們将在本次研讨會的其餘部分使用它。 |
5.3.2。分析個人資料
現在我們有了一個配置檔案,我們可以
go tool pprof
用來分析它。
% go tool pprof -http=:8080 cpu.pprof
在這次運作中,我們看到程式運作了1.81秒(分析增加了一小部分開銷)。我們還可以看到pprof僅捕獲資料1.53秒,因為pprof是基于樣本的,依賴于作業系統的
SIGPROF
計時器。
從Go 1.9開始, 跟蹤包含分析跟蹤所需的所有資訊。您不再需要也具有生成跟蹤的比對二進制檔案。🎉 |
我們可以使用
top
pprof函數對跟蹤記錄的函數進行排序
% go tool pprof cpu.pprof
Type: cpu
Time: Mar 24, 2019 at 5:18pm (CET)
Duration: 2.16s, Total samples = 1.91s (88.51%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 1.90s, 99.48% of 1.91s total
Showing top 10 nodes out of 35
flat flat% sum% cum cum%
0.82s 42.93% 42.93% 1.63s 85.34% main.fillPixel
0.81s 42.41% 85.34% 0.81s 42.41% main.paint
0.11s 5.76% 91.10% 0.12s 6.28% runtime.mallocgc
0.04s 2.09% 93.19% 0.04s 2.09% runtime.memmove
0.04s 2.09% 95.29% 0.04s 2.09% runtime.nanotime
0.03s 1.57% 96.86% 0.03s 1.57% runtime.pthread_cond_signal
0.02s 1.05% 97.91% 0.04s 2.09% compress/flate.(*compressor).deflate
0.01s 0.52% 98.43% 0.01s 0.52% compress/flate.(*compressor).findMatch
0.01s 0.52% 98.95% 0.01s 0.52% compress/flate.hash4
0.01s 0.52% 99.48% 0.01s 0.52% image/png.filter
main.fillPixel
當pprof捕獲堆棧時,我們看到該函數在CPU上最多。
main.paint
在堆棧上找到并不奇怪,這就是程式的作用; 它描繪了像素。但是
paint
花了這麼多時間的原因是什麼?我們可以用累積标志來檢查
top
。
(pprof) top --cum
Showing nodes accounting for 1630ms, 85.34% of 1910ms total
Showing top 10 nodes out of 35
flat flat% sum% cum cum%
0 0% 0% 1840ms 96.34% main.main
0 0% 0% 1840ms 96.34% runtime.main
820ms 42.93% 42.93% 1630ms 85.34% main.fillPixel
0 0% 42.93% 1630ms 85.34% main.seqFillImg
810ms 42.41% 85.34% 810ms 42.41% main.paint
0 0% 85.34% 210ms 10.99% image/png.(*Encoder).Encode
0 0% 85.34% 210ms 10.99% image/png.Encode
0 0% 85.34% 160ms 8.38% main.(*img).At
0 0% 85.34% 160ms 8.38% runtime.convT2Inoptr
0 0% 85.34% 150ms 7.85% image/png.(*encoder).writeIDATs
這有點暗示
main.fillPixed
實際上正在完成大部分工作。
您還可以使用 指令可視化配置檔案,如下所示: Type: cpuTime: Sep 17, 2017 at 12:22pm (AEST)Duration: 1.81s, Total samples = 1.53s (84.33%)Showing nodes accounting for 1.53s, 100% of 1.53s totalmainpaintmandelbrot.go1s (65.36%)runtimemainproc.go0 of 1.53s (100%)mainmainmandelbrot.go0 of 1.53s (100%)1.53smainfillPixelmandelbrot.go0.27s (17.65%)of 1.27s (83.01%)1s(inline)image/pngEncodewriter.go0 of 0.26s (16.99%)0.26smainseqFillImgmandelbrot.go0 of 1.27s (83.01%)1.27sruntimemallocgcmalloc.go0.13s (8.50%)of 0.16s (10.46%)runtime(*mcache)nextFreemalloc.go0 of 0.03s (1.96%)0.03simage/png(*encoder)writeImagewriter.go0 of 0.19s (12.42%)main(*img)Atmandelbrot.go0 of 0.18s (11.76%)0.11simage/pngfilterwriter.go0.01s (0.65%)0.01scompress/zlib(*Writer)Writewriter.go0 of 0.07s (4.58%)0.07simage/png(*Encoder)Encodewriter.go0 of 0.26s (16.99%)image/png(*encoder)writeIDATswriter.go0 of 0.19s (12.42%)0.19simage/pngopaquewriter.go0 of 0.07s (4.58%)0.07sruntimeconvT2Inoptriface.go0 of 0.18s (11.76%)0.18ssyscallSyscallasm_darwin_amd64.s0.05s (3.27%)0.16sruntimememmovememmove_amd64.s0.02s (1.31%)0.02scompress/flate(*compressor)deflatedeflate.go0.01s (0.65%)of 0.07s (4.58%)compress/flate(*compressor)findMatchdeflate.go0 of 0.01s (0.65%)0.01scompress/flate(*compressor)writeBlockdeflate.go0 of 0.05s (3.27%)0.05sruntimemmapsys_darwin_amd64.s0.02s (1.31%)compress/flate(*huffmanBitWriter)writehuffman_bit_writer.go0 of 0.05s (3.27%)compress/flate(*dictWriter)Writedeflate.go0 of 0.05s (3.27%)0.05scompress/flate(*huffmanBitWriter)writeTokenshuffman_bit_writer.go0 of 0.05s (3.27%)compress/flate(*huffmanBitWriter)writeBitshuffman_bit_writer.go0 of 0.01s (0.65%)0.01scompress/flate(*huffmanBitWriter)writeCodehuffman_bit_writer.go0 of 0.04s (2.61%)0.04sruntimesystemstackasm_amd64.s0 of 0.03s (1.96%)runtime(*mcache)nextFreefunc1malloc.go0 of 0.02s (1.31%)0.02sruntime(*mheap)allocfunc1mheap.go0 of 0.01s (0.65%)0.01scompress/flatematchLendeflate.go0.01s (0.65%)runtime(*mcentral)growmcentral.go0 of 0.02s (1.31%)runtime(*mheap)allocmheap.go0 of 0.01s (0.65%)0.01sruntimeheapBitsinitSpanmbitmap.go0 of 0.01s (0.65%)0.01sruntimememclrNoHeapPointersmemclr_amd64.s0.01s (0.65%)bufio(*Writer)Flushbufio.go0 of 0.05s (3.27%)image/png(*encoder)Writewriter.go0 of 0.05s (3.27%)0.05sbufio(*Writer)Writebufio.go0 of 0.05s (3.27%)0.05scompress/flate(*Writer)Writedeflate.go0 of 0.07s (4.58%)compress/flate(*compressor)writedeflate.go0 of 0.07s (4.58%)0.07s0.01s0.07scompress/flate(*huffmanBitWriter)writeBlockhuffman_bit_writer.go0 of 0.05s (3.27%)0.05s0.05s0.01s0.05s0.04s0.07simage/png(*encoder)writeChunkwriter.go0 of 0.05s (3.27%)0.05sos(*File)Writefile.go0 of 0.05s (3.27%)0.05s0.19s0.26s0.07sinternal/poll(*FD)Writefd_unix.go0 of 0.05s (3.27%)syscallWritesyscall_unix.go0 of 0.05s (3.27%)0.05s1.27sos(*File)writefile_unix.go0 of 0.05s (3.27%)0.05s0.05s0.03sruntime(*mcache)refillmcache.go0 of 0.02s (1.31%)0.02sruntime(*mcentral)cacheSpanmcentral.go0 of 0.02s (1.31%)0.02s0.02s0.01sruntime(*mheap)alloc_mmheap.go0 of 0.01s (0.65%)0.01sruntime(*mheap)allocSpanLockedmheap.go0 of 0.01s (0.65%)runtime(*mheap)growmheap.go0 of 0.01s (0.65%)0.01s0.01sruntime(*mheap)sysAllocmalloc.go0 of 0.01s (0.65%)0.01sruntimesysMapmem_darwin.go0 of 0.01s (0.65%)0.01sruntimenewMarkBitsmheap.go0 of 0.01s (0.65%)0.01sruntimenewArenaMayUnlockmheap.go0 of 0.01s (0.65%)runtimesysAllocmem_darwin.go0 of 0.01s (0.65%)0.01s0.01s0.01s0.01ssyscallwritezsyscall_darwin_amd64.go0 of 0.05s (3.27%)0.05s0.05s |
5.4。跟蹤與分析
希望這個例子顯示了分析的局限性。剖析告訴我們剖面儀看到了什麼;
fillPixel
正在做所有的工作。看起來沒有那麼多可以做的事情。
是以現在是引入執行跟蹤器的好時機,它給出了同一程式的不同視圖。
5.4.1。使用執行跟蹤器
使用跟蹤器就像要求一樣簡單
profile.TraceProfile
,沒有其他任何改變。
import "github.com/pkg/profile"
func main() {
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
當我們運作程式時,我們
trace.out
在目前工作目錄中擷取一個檔案。
% go build mandelbrot.go
% % time ./mandelbrot
2017/09/17 13:19:10 profile: trace enabled, trace.out
2017/09/17 13:19:12 profile: trace disabled, trace.out
real 0m1.740s
user 0m1.707s
sys 0m0.020s
就像pprof一樣,
go
指令中有一個工具來分析跟蹤。
% go tool trace trace.out
2017/09/17 12:41:39 Parsing trace...
2017/09/17 12:41:40 Serializing trace...
2017/09/17 12:41:40 Splitting trace...
2017/09/17 12:41:40 Opening browser. Trace viewer s listening on http://127.0.0.1:57842
這個工具有點不同
go tool pprof
。執行跟蹤器正在重複使用Chrome中内置的大量配置檔案可視化基礎架構,是以
go tool trace
充當伺服器将原始執行跟蹤轉換為Chome可以本機顯示的資料。
5.4.2。分析痕迹
我們可以從跟蹤中看到程式隻使用一個cpu。
func seqFillImg(m *img) {
for i, row := range m.m {
for j := range row {
fillPixel(m, i, j)
}
}
}
這并不奇怪,預設情況下按順序
mandelbrot.go
調用
fillPixel
每一行中的每個像素。
繪制圖像後,請參閱執行開關以寫入
.png
檔案。這會在堆上生成垃圾,是以跟蹤在此時發生變化,我們可以看到垃圾收集堆的經典鋸齒模式。
跟蹤配置檔案提供低至微秒級别的定時分辨率。這是您通過外部分析無法獲得的。
去工具痕迹 在我們繼續之前,我們應該談談跟蹤工具的使用。
|
5.5。使用多個CPU
我們從前面的跟蹤中看到,程式正在按順序運作,而不是利用此計算機上的其他CPU。
Mandelbrot一代被稱為embarassingly_parallel。每個像素都是獨立的,它們都可以并行計算。那麼,讓我們試試吧。
% go build mandelbrot.go
% time ./mandelbrot -mode px
2017/09/17 13:19:48 profile: trace enabled, trace.out
2017/09/17 13:19:50 profile: trace disabled, trace.out
real 0m1.764s
user 0m4.031s
sys 0m0.865s
是以運作時基本相同。有更多的使用者時間,這是有道理的,我們使用所有的CPU,但實際(挂鐘)時間大緻相同。
讓我們來看看。
如您所見,此跟蹤生成了更多資料。
- 看起來很多工作正在完成,但如果你放大,就會有差距。這被認為是排程程式。
- 雖然我們使用所有四個核心,因為每個核心
的工作量相對較小,但我們在排程開銷方面花費了大量時間。fillPixel
5.6。批量工作
每個像素使用一個goroutine太精細了。沒有足夠的工作來證明goroutine的成本。
相反,讓我們嘗試每個goroutine處理一行。
% go build mandelbrot.go
% time ./mandelbrot -mode row
2017/09/17 13:41:55 profile: trace enabled, trace.out
2017/09/17 13:41:55 profile: trace disabled, trace.out
real 0m0.764s
user 0m1.907s
sys 0m0.025s
這看起來是一個很好的改進,我們差不多将程式的運作時間減半。我們來看看這條痕迹。
正如您所看到的,跟蹤現在更小,更易于使用。我們可以看到整個軌迹,這是一個很好的獎勵。
- 在程式開始時,我們看到goroutines的數量增加到大約1,000。這是我們在前面的描述中看到的1 << 20的改進。
- 放大我們看到
運作時間更長,并且由于goroutine 生産工作提前完成,排程程式有效地通過剩餘的可運作的goroutine。onePerRowFillImg
5.7。使用勞工
mandelbrot.go
支援另一種模式,讓我們嘗試一下。
% go build mandelbrot.go
% time ./mandelbrot -mode workers
2017/09/17 13:49:46 profile: trace enabled, trace.out
2017/09/17 13:49:50 profile: trace disabled, trace.out
real 0m4.207s
user 0m4.459s
sys 0m1.284s
是以,運作時比以前任何時候都要糟糕得多。讓我們看看跟蹤,看看我們是否能弄清楚發生了什麼。
檢視跟蹤,您可以看到,隻有一個工作程序,生産者和消費者傾向于交替,因為隻有一個工作者和一個消費者。讓我們增加勞工數量
% go build mandelbrot.go
% time ./mandelbrot -mode workers -workers 4
2017/09/17 13:52:51 profile: trace enabled, trace.out
2017/09/17 13:52:57 profile: trace disabled, trace.out
real 0m5.528s
user 0m7.307s
sys 0m4.311s
這讓事情變得更糟!更實時,更多的CPU時間。讓我們看看跟蹤,看看發生了什麼。
那條痕迹是一團糟。有更多的勞工可用,但似乎花了他們所有的時間來争取工作。
這是因為通道是無緩沖的。在有人準備好接收之前,無法發送無緩沖的頻道。
- 生産者在勞工準備接收工作之前不能發送工作。
- 從業人員在有人準備發送之前無法接收工作,是以他們在等待時互相競争。
- 發件人沒有特權,它不能優先于已經運作的從業人員。
我們在這裡看到的是無緩沖通道引入的大量延遲。排程程式内部有很多停止和啟動,并且在等待工作時可能會鎖定和互斥,這就是我們看到
sys
時間更長的原因。
5.8。使用緩沖的通道
import "github.com/pkg/profile"
func main() {
defer profile.Start(profile.TraceProfile, profile.ProfilePath(".")).Stop()
% go build mandelbrot.go
% time ./mandelbrot -mode workers -workers 4
2017/09/17 14:23:56 profile: trace enabled, trace.out
2017/09/17 14:23:57 profile: trace disabled, trace.out
real 0m0.905s
user 0m2.150s
sys 0m0.121s
這與上面的每行模式非常接近。
使用緩沖通道,跟蹤向我們顯示:
- 生産者不必等待勞工到達,它可以快速填滿管道。
- 勞工可以快速從通道中取出下一個項目,而無需等待工作生成。
使用這種方法,我們獲得了幾乎相同的速度,使用一個通道來切換每個像素的工作,而不是之前在每行goroutine上排程。
修改
nWorkersFillImg
為每行工作。計算結果并分析跟蹤。
5.9。Mandelbrot微服務
它是2019年,生成Mandelbrots毫無意義,除非你可以在網際網路上提供它們作為無伺服器的微服務。是以,我向你呈現Mandelweb
% go run examples/mandelweb/mandelweb.go
2017/09/17 15:29:21 listening on http://127.0.0.1:8080/
http://127.0.0.1:8080/mandelbrot
5.9.1。跟蹤正在運作的應用
在前面的示例中,我們在整個程式中運作了跟蹤。
如您所見,即使在很短的時間内,跟蹤也可能非常大,是以不斷收集跟蹤資料會産生太多資料。此外,跟蹤可能會對程式的速度産生影響,尤其是在有大量活動的情況下。
我們想要的是一種從正在運作的程式中收集短跟蹤的方法。
很有可能,
net/http/pprof
包裝就是這樣的設施。
5.9.2。通過http收集痕迹
希望每個人都知道
net/http/pprof
包裝。
import _ "net/http/pprof"
導入時,
net/http/pprof
将使用注冊跟蹤和分析路由
http.DefaultServeMux
。從Go 1.5開始,這包括跟蹤分析器。
注冊 。如果您 隐式或明确地使用它,您可能會無意中将pprof端點暴露給Internet。這可能導緻源代碼洩露。你可能不想這樣做。 |
我們可以用
curl
(或
wget
)從mandelweb擷取五秒鐘的痕迹
% curl -o trace.out http://127.0.0.1:8080/debug/pprof/trace?seconds=5
5.9.3。生成一些負載
前面的示例很有趣,但根據定義,空閑的Web伺服器沒有性能問題。我們需要産生一些負載。為此,我正在使用heyJBD。
% go get -u github.com/rakyll/hey
讓我們從每秒一個請求開始。
% hey -c 1 -n 1000 -q 1 http://127.0.0.1:8080/mandelbrot
随着運作,在另一個視窗收集跟蹤
% curl -o trace.out http://127.0.0.1:8080/debug/pprof/trace?seconds=5
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 66169 0 66169 0 0 13233 0 --:--:-- 0:00:05 --:--:-- 17390
% go tool trace trace.out
2017/09/17 16:09:30 Parsing trace...
2017/09/17 16:09:30 Serializing trace...
2017/09/17 16:09:30 Splitting trace...
2017/09/17 16:09:30 Opening browser.
Trace viewer is listening on http://127.0.0.1:60301
5.9.4。模拟過載
讓我們将速率提高到每秒5個請求。
% hey -c 5 -n 1000 -q 5 http://127.0.0.1:8080/mandelbrot
随着運作,在另一個視窗收集跟蹤
%curl -o trace.out http://127.0.0.1:8080/debug/pprof/trace?seconds=5
%總收到百分比%Xferd平均速度時間時間目前時間
Dload上載總左轉速度
100 66169 0 66169 0 0 13233 0 - : - : - 0:00:05 - : - : - 17390
%go工具跟蹤trace.out
2017/09/17 16:09:30解析痕迹......
2017/09/17 16:09:30序列化痕迹......
2017/09/17 16:09:30分裂痕迹......
2017/09/17 16:09:30打開浏覽器。跟蹤檢視器正在偵聽http://127.0.0.1:60301
5.9.5。額外的功勞,Eratosthenes的Sieve
在并發素篩是寫入的第一個圍棋程式之一。
Ivan Daniluk 撰寫了一篇關于可視化的精彩文章。
讓我們看一下使用執行跟蹤器的操作。
5.9.6。更多資源
- Rhys Hiltner,Go的執行追蹤者(dotGo 2016)
- Rhys Hiltner,“go tool trace”簡介(GopherCon 2017)
- Dave Cheney,介紹Go項目的七種方式(GolangUK 2016)
- Dave Cheney,高績效圍棋研讨會 ]
- Ivan Daniluk,在Go中可視化并發(GopherCon 2016)
- Kavya Joshi,了解頻道(GopherCon 2017)
- Francesc Campoy,使用Go執行跟蹤器
6.記憶體和垃圾收集器
Go是一種垃圾收集語言。這是一個設計原則,它不會改變。
作為垃圾收集語言,Go程式的性能通常取決于它們與垃圾收集器的互動。
在您選擇的算法旁邊,記憶體消耗是決定應用程式性能和可伸縮性的最重要因素。
本節讨論垃圾收集器的操作,如何測量程式的記憶體使用情況以及在垃圾收集器性能是瓶頸時降低記憶體使用率的政策。
6.1。垃圾收集器世界觀
任何垃圾收集器的目的都是為了表明程式可以使用無限量的記憶體。
您可能不同意這種說法,但這是垃圾收集器設計者如何工作的基本假設。
停止世界,标記掃描GC在總運作時間方面是最有效的; 适用于批處理,模拟等。但是,随着時間的推移,Go GC已經從純粹的世界收集器轉變為并發的非壓縮收集器。這是因為Go GC專為低延遲伺服器和互動式應用程式而設計。
Go GC的設計傾向于lower_latency而不是maximum_throughput ; 它将一些配置設定成本轉移到mutator以降低以後的清理成本。
6.2。垃圾收集器設計
多年來,Go GC的設計發生了變化
- 去1.0,嚴重依賴tcmalloc停止世界标記掃描收集器。
- 去1.3,完全精确的收集器,不會将堆上的大數字誤認為指針,進而洩漏記憶體。
- Go 1.5,新的GC設計,專注于延遲超過吞吐量。
- 進行1.6,GC改進,處理更大的堆,延遲更低。
- 去1.7,小GC改進,主要是重構。
- 進入1.8,進一步減少STW時間,現在降至100微秒範圍。
- 轉到1.10+,遠離純粹的cooprerative goroutine排程,以在觸發完整GC循環時降低延遲。
6.3。垃圾收集器監控
獲得垃圾收集器工作難度的一般概念的簡單方法是啟用GC日志記錄的輸出。
始終收集這些統計資訊,但通常會被抑制,您可以通過設定
GODEBUG
環境變量來啟用它們的顯示。
% env GODEBUG=gctrace=1 godoc -http=:8080
gc 1 @0.012s 2%: 0.026+0.39+0.10 ms clock, 0.21+0.88/0.52/0+0.84 ms cpu, 4->4->0 MB, 5 MB goal, 8 P
gc 2 @0.016s 3%: 0.038+0.41+0.042 ms clock, 0.30+1.2/0.59/0+0.33 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 3 @0.020s 4%: 0.054+0.56+0.054 ms clock, 0.43+1.0/0.59/0+0.43 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 4 @0.025s 4%: 0.043+0.52+0.058 ms clock, 0.34+1.3/0.64/0+0.46 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 5 @0.029s 5%: 0.058+0.64+0.053 ms clock, 0.46+1.3/0.89/0+0.42 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 6 @0.034s 5%: 0.062+0.42+0.050 ms clock, 0.50+1.2/0.63/0+0.40 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 7 @0.038s 6%: 0.057+0.47+0.046 ms clock, 0.46+1.2/0.67/0+0.37 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 8 @0.041s 6%: 0.049+0.42+0.057 ms clock, 0.39+1.1/0.57/0+0.46 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
gc 9 @0.045s 6%: 0.047+0.38+0.042 ms clock, 0.37+0.94/0.61/0+0.33 ms cpu, 4->4->1 MB, 5 MB goal, 8 P
跟蹤輸出提供GC活動的一般度量。的輸出格式
gctrace=1
中描述的runtime封包檔。
DEMO:顯示
godoc
與
GODEBUG=gctrace=1
啟用
在生産中使用此env var,它沒有性能影響。 |
GODEBUG=gctrace=1
當您知道存在問題時使用很好,但對于Go應用程式的一般遙測,我推薦使用該
net/http/pprof
接口。
import _ "net/http/pprof"
導入
net/http/pprof
包将
/debug/pprof
使用各種運作時名額注冊處理程式,包括:
- 所有正在運作的goroutine的清單
。/debug/pprof/heap?debug=1
- 關于記憶體配置設定統計的報告,
。/debug/pprof/heap?debug=1
将使用您的預設值注冊自己 。 請注意,如果您使用,這将是可見的 。 |
示範:
godoc -http=:8080
,顯示
/debug/pprof
。
6.3.1。垃圾收集器調整
Go運作時提供了一個環境變量來調整GC
GOGC
。
GOGC的公式是
g o a l= re a c h a b lë ⋅ ( 1 + G ^ Ö ģ Ç100)GØ一個升=[RË一個CH一個b升Ë⋅(1+GØGC100)
例如,如果我們目前有256MB堆,并且
GOGC=100
(預設值),當堆填滿時,它将增長到
512 米B = 256 M.乙⋅ ( 1 + 100100)512中号乙=256中号乙⋅(1+100100)
-
大于100的值會使堆增長更快,進而降低GC的壓力。GOGC
-
小于100的值會導緻堆緩慢增長,進而增加GC的壓力。GOGC
預設值100是just_a_guide。在使用生産負載分析應用程式後,您應該選擇自己的值。
6.4。減少配置設定
確定您的API允許調用者減少生成的垃圾量。
考慮這兩種Read方法
func (r *Reader) Read() ([]byte, error)
func (r *Reader) Read(buf []byte) (int, error)
第一個Read方法不帶參數,并傳回一些資料作為
[]byte
。第二個采用
[]byte
緩沖區并傳回讀取的位元組數。
第一個Read方法總是會配置設定一個緩沖區,給GC帶來壓力。第二個填充它給出的緩沖區。
你能在std lib中命名這個模式的例子嗎?
6.5。字元串和[]位元組
在Go中,
string
值是不可變的,
[]byte
是可變的。
大多數程式都喜歡工作
string
,但大多數IO都是完成的
[]byte
。
避免
[]byte
在可能的情況下進行字元串轉換,這通常意味着選擇一個表示,a
string
或a
[]byte
表示值。通常情況下,
[]byte
如果您從網絡或磁盤讀取資料。
該bytes軟體包包含許多相同的操作- ,
Split
,
Compare
,
HasPrefix
,
Trim
等-作為strings包裝。
引擎蓋下
strings
使用與
bytes
包相同的元件原語。
6.6。使用[]byte作為地圖的關鍵
使用a
string
作為地圖鍵是很常見的,但通常你有一個
[]byte
。
編譯器為此案例實作了特定的優化
var m map[string]string
v, ok := m[string(bytes)]
這将避免将位元組切片轉換為字元串以進行地圖查找。這是非常具體的,如果你這樣做,它将無法工作
key := string(bytes)
val, ok := m[key]
讓我們看看這是否仍然存在。編寫一個基準,比較使用a
[]byte
作為
string
映射鍵的這兩種方法。
6.7。避免字元串連接配接
Go字元串是不可變的。連接配接兩個字元串會産生第三個字元串。以下哪項最快?
s := request.ID
s += " " + client.Addr().String()
s += " " + time.Now().String()
r = s
var b bytes.Buffer
fmt.Fprintf(&b, "%s %v %v", request.ID, client.Addr(), time.Now())
r = b.String()
r = fmt.Sprintf("%s %v %v", request.ID, client.Addr(), time.Now())
b := make([]byte, 0, 40)
b = append(b, request.ID...)
b = append(b, ' ')
b = append(b, client.Addr().String()...)
b = append(b, ' ')
b = time.Now().AppendFormat(b, "2006-01-02 15:04:05.999999999 -0700 MST")
r = string(b)
var b strings.Builder
b.WriteString(request.ID)
b.WriteString(" ")
b.WriteString(client.Addr().String())
b.WriteString(" ")
b.WriteString(time.Now().String())
r = b.String()
DEMO:
go test -bench=. ./examples/concat
6.8。如果長度已知,則預配置設定切片
追加友善,但浪費。
切片增加倍數達到1024個元素,然後增加約25%。
b
在我們追加一件商品之後的容量是多少?
func main() {
b := make([]int, 1024)
b = append(b, 99)
fmt.Println("len:", len(b), "cap:", cap(b))
}
如果使用追加模式,則可能會複制大量資料并造成大量垃圾。
如果事先知道切片的長度,則預先配置設定目标以避免複制并確定目标的大小正确。
之前
var s []string
for _, v := range fn() {
s = append(s, v)
}
return s
後
vals := fn()
s := make([]string, len(vals))
for i, v := range vals {
s[i] = v
}
return s
6.9。使用sync.Pool
該
sync
軟體包附帶一個
sync.Pool
用于重用常見對象的類型。
sync.Pool
沒有固定的大小或最大容量。您添加它并從中取出直到GC發生,然後無條件地清空它。這是設計的:
如果在垃圾收集過早之前和垃圾收集太晚之後,那麼排空池的正确時間必須在垃圾收集期間。也就是說,Pool類型的語義必須是它在每個垃圾收集時消失。 - 拉斯考克斯
sync.Pool在行動中
var pool = sync.Pool{New: func() interface{} { return make([]byte, 4096) }}
func fn() {
buf := pool.Get().([]byte) // takes from pool or calls New
// do work
pool.Put(buf) // returns buf to the pool
}
不是緩存。它可以并且将在at_any_time清空。 不要将重要物品放入 ,它們将被丢棄。 |
在每個GC上清空自己的sync.Pool的設計可能會在Go 1.13中改變,這将有助于提高其效用。 此CL通過引入受害者緩存機制來解決此問題。不會清除池,而是删除受害者緩存,并将主緩存移動到受害者緩存。是以,在穩定狀态下,(大緻)沒有新的配置設定,但如果池使用率下降,則仍将在兩個GC(而不是一個)中收集對象。 - 奧斯汀克萊門茨https://go-review.googlesource.com/c/go/+/166961/ |
6.10。演習
- 使用
(或其他程式)觀察更改godoc
使用的結果GOGC
。GODEBUG=gctrace=1
- 基準位元組的字元串(位元組)映射鍵
- 基準來自不同的concat政策。
7.提示和旅行
随機抓取提示和建議
最後一節包含一些微優化Go代碼的技巧。
7.1。夠程
Go的關鍵特性使其非常适合現代硬體,這些都是goroutines。
Goroutines很容易使用,而且建立起來很便宜,你可以認為它們幾乎是免費的。
Go運作時是為具有成千上萬個goroutines的程式編寫的,數十萬不是意料之外的。
但是,每個goroutine确實消耗了goroutine堆棧的最小記憶體量,目前至少為2k。
2048 * 1,000,000 goroutines == 2GB的記憶體,他們還沒有做任何事情。
也許這是很多,也許它沒有給出你的應用程式的其他用法。
7.1.1。知道什麼時候停止goroutine
Goroutines起步便宜且運作成本低廉,但它們在記憶體占用方面的成本确實有限; 你無法創造無限數量的它們。
每次
go
在程式中使用關鍵字來啟動goroutine時,都必須知道 goroutine将如何以及何時退出。
在您的設計中,一些goroutine可能會運作直到程式退出。這些goroutine很少見,不會成為規則的例外。
如果您不知道答案,那就是潛在的記憶體洩漏,因為goroutine會将其堆棧的記憶體固定在堆上,以及從堆棧可以通路的任何堆配置設定的變量。
永遠不要在不知道如何停止的情況下啟動goroutine。 |
7.1.2。進一步閱讀
- 并發變得容易(視訊)
- 并發變得容易(幻燈片)
- 如果不知道它什麼時候停止就不要開始goroutine(Practical Go,QCon Shanghai 2018)
7.2。Go對某些請求使用高效的網絡輪詢
Go運作時使用高效的作業系統輪詢機制(kqueue,epoll,windows IOCP等)處理網絡IO。許多等待的goroutine将由單個作業系統線程提供服務。
但是,對于本地檔案IO,Go不實作任何IO輪詢。a上的每個操作
*os.File
在進行時消耗一個作業系統線程。
大量使用本地檔案IO會導緻程式産生數百或數千個線程; 可能超過您的作業系統允許。
您的磁盤子系統不希望能夠處理數百或數千個并發IO請求。
要限制并發阻塞IO的數量,請使用worker goroutines池或緩沖通道作為信号量。 |
7.3。注意應用程式中的IO乘數
如果您正在編寫伺服器程序,那麼它的主要工作是複用通過網絡連接配接的用戶端以及存儲在應用程式中的資料。
大多數伺服器程式接受請求,進行一些處理,然後傳回結果。這聽起來很簡單,但根據結果,它可以讓用戶端在伺服器上消耗大量(可能無限制)的資源。以下是一些需要注意的事項:
- 每個傳入請求的IO請求數量; 單個用戶端請求生成多少個IO事件?如果從緩存中提供多個請求,則它可能平均為1,或者可能小于1。
- 服務查詢所需的讀取量; 它是固定的,N + 1還是線性的(讀取整個表格以生成結果的最後一頁)。
如果記憶體很慢,相對來說,那麼IO太慢了,你應該不惜一切代價避免這樣做。最重要的是避免在請求的上下文中執行IO - 不要讓使用者等待磁盤子系統寫入磁盤,甚至不要讀取。
7.4。使用流式IO接口
盡可能避免将資料讀入
[]byte
并傳遞給它。
根據請求,您最終可能會将兆位元組(或更多!)的資料讀入記憶體。這給GC帶來了巨大壓力,這将增加應用程式的平均延遲。
而是使用
io.Reader
和
io.Writer
構造處理管道來限制每個請求使用的記憶體量。
為了提高效率,請考慮實施
io.ReaderFrom
/
io.WriterTo
如果您使用了很多
io.Copy
。這些接口更有效,并避免将記憶體複制到臨時緩沖區。
7.5。逾時,逾時,逾時
在不知道最長時間的情況下,切勿啟動IO操作。
您需要設定逾時你讓每網絡請求
SetDeadline
,
SetReadDeadline
,
SetWriteDeadline
。
7.6。推遲是昂貴的,或者是它?
defer
是昂貴的,因為它必須記錄延遲的論點的閉包。
defer mu.Unlock()
相當于
defer func() {
mu.Unlock()
}()
defer
如果正在完成的工作量很小,那麼經典的例子就是
defer
圍繞結構變量或地圖查找進行互斥鎖解鎖。
defer
在這些情況下,您可以選擇避免。
這是為了獲得性能而犧牲可讀性和維護性的情況。
始終重新審視這些決定。
7.7。避免終結者
最終化是一種将行為附加到即将被垃圾收集的對象的技術。
是以,最終确定是非确定性的。
要運作終結器,任何東西都不能通路該對象。如果您不小心在地圖中保留了對象的引用,則無法完成。
終結者作為gc循環的一部分運作,這意味着它們在運作時是不可預測的,并且使它們與減少gc操作的目标不一緻。
如果你有一個大堆并且已經調整你的應用程式來建立最小的垃圾,終結者可能不會運作很長時間。
7.8。最小化cgo
cgo允許Go程式調用C庫。
C代碼和Go代碼存在于兩個不同的Universe中,cgo周遊它們之間的邊界。
這種轉換不是免費的,取決于代碼中的位置,成本可能很高。
cgo調用類似于阻塞IO,它們在操作期間消耗一個線程。
不要在緊密循環中調用C代碼。
7.8.1。實際上,也許避免使用cgo
cgo的開銷很高。
為獲得最佳性能,我建議您在應用程式中避免使用cgo
- 如果C代碼需要很長時間,那麼cgo開銷就不那麼重要了。
- 如果你正在使用cgo來調用一個非常短的C函數,其中開銷是最明顯的,那麼在Go中重寫該代碼 - 根據定義它很短。
- 如果您使用大量昂貴的C代碼在緊密循環中調用,為什麼使用Go?
是否有人使用cgo經常撥打昂貴的C代碼?
進一步閱讀
- cgo不是Go
7.9。始終使用最新釋出的Go版本
Go的舊版本永遠不會變得更好。他們永遠不會得到錯誤修複或優化。
- 不應該使用Go 1.4。
- Go 1.5和1.6的編譯器速度較慢,但它産生更快的代碼,并且具有更快的GC。
- Go 1.7的編譯速度比1.6提高了大約30%,連結速度提高了2倍(優于之前的Go版本)。
- Go 1.8将提高編譯速度(此時),但非英特爾架構的代碼品質有了顯着提高。
- 轉到1.9-1.12繼續提高生成代碼的性能,修複錯誤,改進内聯并改進debuging。
Go的舊版本沒有收到任何更新。不要使用它們。使用最新版本,您将獲得最佳性能。 |
7.9.1。進一步閱讀
- 去1.7工具鍊改進
- 1.8性能改進
7.9.2。将熱字段移動到結構的頂部
7.10。讨論
任何問題?
最後的問題和結論
可讀性意味着可靠 - Rob Pike
從最簡單的代碼開始。
測量。描述您的代碼以識别瓶頸,不要猜測。
如果表現良好,請停止。您不需要優化所有内容,隻需要優化代碼中最熱門的部分。
随着應用程式的增長或流量模式的發展,性能熱點将會發生變化。
不要留下對性能不重要的複雜代碼,如果瓶頸移到其他地方,則用更簡單的操作重寫它。
總是編寫最簡單的代碼,編譯器針對普通代碼進行了優化。
更短的代碼是更快的代碼; Go不是C ++,不要指望編譯器解開複雜的抽象。
更短的代碼是更小的代碼; 這對CPU的緩存很重要。
密切關注配置設定,盡可能避免不必要的配置設定。
如果他們不必正确,我可以把事情做得很快。 - 拉斯考克斯
性能和可靠性同樣重要。
我認為制作速度非常快的伺服器,定期出現恐慌,死鎖或OOM的價值不大。
不要為了可靠性而交易性能。
1。Hennessy等人:超過40年的1.4倍年度績效改進。
最後更新日期2019-04-26 02:55:54
作者:sunsky303