高性能Golang研討會【精】


概觀

本次研討會的目標是為您提供診斷Go應用程序中的性能問題並進行修復所需的工具。

通過這一天,我們將從小工作 - 學習如何編寫基准,然后分析一小段代碼。然后走出去討論執行跟蹤器,垃圾收集器和跟蹤運行的應用程序。剩下的時間將是您提出問題的機會,並嘗試使用您自己的代碼。

 

您可以在此處找到此演示文稿的最新版本

 

歡迎

你好,歡迎光臨!🎉

本次研討會的目標是為您提供診斷Go應用程序中的性能問題並進行修復所需的工具。

通過這一天,我們將從小工作 - 學習如何編寫基准,然后分析一小段代碼。然后走出去討論執行跟蹤器,垃圾收集器和跟蹤運行的應用程序。剩下的時間將是您提出問題的機會,並嘗試使用您自己的代碼。

教師

許可證和材料

本次研討會是David CheneyFrancesc Campoy的合作

本演示文稿根據知識共享署名 - 相同方式共享4.0國際許可進行許可。

先決條件

這是您今天需要的幾個軟件下載。

研討會資料庫

將源代碼下載到本文檔並在https://github.com/davecheney/high-performance-go-workshop上編寫代碼示例

筆記本電腦,電源等

研討會的材料針對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。理解機械

圖像20180818145606919

目前有一個流行的術語,你會聽到Martin Thompson或Bill Kennedy等人談論“機械上的理解”。

“機械理解”這個名字來自偉大的賽車手傑基斯圖爾特,他是世界一級方程式賽車冠軍的3倍。他相信最好的車手對機器如何工作有足夠的了解,因此他們可以與之協調工作。

要成為一名優秀的賽車手,你不需要成為一名出色的機械師,但你需要對馬車的工作方式有一個粗略的了解。

我相信我們作為軟件工程師也是如此。我認為這個會議室里的任何人都不會成為專業的CPU設計師,但這並不意味着我們可以忽略CPU設計人員面臨的問題。

1.2。六個數量級

有一個常見的互聯網模因,就像這樣;

jalopnik

當然這是荒謬的,但它強調了計算行業的變化。

作為軟件作者,我們這個房間里的所有人都受益於摩爾定律,40年來,芯片每18個月可用晶體管數量翻了一番。沒有其他行業在一生的空間中經歷了六個數量級的工具改進1 ]

但這一切都在改變。

1.3。電腦還在變快嗎?

因此,基本問題是,面對上圖中的統計數據,我們應該問的問題是計算機是否仍然變得更快

如果計算機仍然變得越來越快,那么我們可能不需要關心代碼的性能,我們只需稍等一下,硬件制造商將為我們解決性能問題。

1.3.1。我們來看看數據

這是您在教科書中找到的經典數據,如計算機體系結構, John L. Hennessy和David A. Patterson的定量方法該圖取自第5版

2313.processorperf

在第5版中,Hennessey和Patterson認為計算性能有三個時代

  • 第一個是1970年代和80年代初,這是形成時期。我們今天所知的微處理器並不存在,計算機是用分立晶體管或小規模集成電路構建的。成本,規模和對材料科學理解的限制是限制因素。

  • 從80年代中期到2004年,趨勢線很明顯。計算機整數性能平均每年提高52%。計算機功率每兩年翻一番,因此人們將摩爾定律與計算機性能相加,即模具上晶體管數量增加一倍。

  • 然后我們來到計算機性能的第三個時代。改進變慢了。總變化率為每年22%。

之前的圖表僅上升到2012年,但幸運的是在2012年,Jeff Preshing編寫了一個工具來抓取Spec網站並構建自己的圖表

int圖

所以這是使用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逆變器

CMOS器件的功耗,就是這個房間里的每個晶體管,桌面和口袋里的三個因素的組合。

  1. 靜電。當晶體管靜止時,即不改變其狀態時,有少量電流通過晶體管泄漏到地。晶體管越小,泄漏越多。泄漏隨溫度升高而增加。當你擁有數十億個晶體管時,即使是少量的泄漏也會增加!

  2. 動力。當晶體管從一種狀態轉換到另一種狀態時,它必須對連接到柵極的各種電容充電或放電。每個晶體管的動態功率是電容的平方乘以電容和變化的頻率。降低電壓可以降低晶體管消耗的功率,但是較低的電壓會導致晶體管切換較慢。

  3. 撬棍或短路電流。我們喜歡將晶體管視為數字設備占據一個或另一個狀態,原子地關閉或打開。實際上,晶體管是模擬器件。作為開關,晶體管大部分開始關斷,並且轉換或切換到大部分開啟的狀態這種轉換或切換時間非常快,在現代處理器中它的速度為皮秒,但仍然代表從Vcc到地的低電阻路徑的一段時間。晶體管開關越快,其頻率越高,散熱量就越大。

1.6。Dennard縮放的結束

為了理解接下來發生的事情,我們需要查看1974年由Robert H. Dennard共同撰寫的論文Dennard的Scaling定律大致指出隨着晶體管變小,它們的功率密度保持不變。較小的晶體管可以在較低的電壓下運行,具有較低的柵極電容,並且開關速度更快,這有助於減少動態功率。

那怎么辦呢?

屏幕截圖2014 04 14 at 8.49.48 AM

結果並不那么好。隨着晶體管的柵極長度接近幾個硅原子的寬度,晶體管尺寸,電壓和重要的泄漏之間的關系被破壞。

在1999年Micro-32會議上假設,如果我們遵循時鍾速度增加和晶體管尺寸縮小的趨勢線,那么在處理器生成中,晶體管結將接近核反應堆核心的溫度。顯然這是瘋狂的。奔騰4 標志着單核,高頻,消費類CPU 的終結

回到這個圖表,我們看到時鍾速度停滯的原因是因為cpu超出了我們冷卻它們的能力。到2006年,減小晶體管的尺寸不再提高其功率效率。

我們現在知道降低CPU特征尺寸主要是為了降低功耗。降低能耗並不僅僅意味着“綠色”,就像回收一樣,拯救地球。主要目標是將功耗和熱耗散保持在低於損壞CPU的水平

口吃

但是,圖表的一部分仍在繼續增加,即芯片上的晶體管數量。cpu的行進特征是在相同的給定區域中具有更大的晶體管,具有正面和負面效果。

此外,正如您在插頁中看到的那樣,每個晶體管的成本持續下降,直到大約5年前,然后每個晶體管的成本開始再次回升。

摩爾定律

創建更小的晶體管不僅成本越來越高,而且越來越難。2016年的這份報告顯示了2013年芯片制造商認為會發生什么的預測; 兩年后,他們錯過了所有的預測,雖然我沒有這份報告的更新版本,但沒有跡象表明他們能夠扭轉這種趨勢。

英特爾,台積電,AMD和三星花費數十億美元,因為他們必須建立新的晶圓廠,購買所有新的工藝工具。因此,雖然每個芯片的晶體管數量持續增加,但其單位成本已開始增加。

 

甚至術語門長度(以納米為單位)也變得模棱兩可。各種制造商以不同的方式測量晶體管的尺寸,使其能夠展示比競爭對手更小的數量,而無需提供。這是CPU制造商的非GAAP收益報告模型。

1.7。更多核心(more cores)

y5cdp7nhs2uy

由於達到了熱量和頻率限制,因此不再能夠使單核運行速度提高兩倍。但是,如果添加其他內核,則可以提供兩倍的處理能力 - 如果軟件可以支持它。

實際上,CPU的核心數量主要是散熱。Dennard縮放的結束意味着CPU的時鍾速度是1到4 Ghz之間的任意數字,具體取決於它的熱度。當我們談論基准測試時,我們會很快看到這一點。

1.8。阿姆達爾定律

CPU不會變得越來越快,但隨着超線程和多核的發展,它們的范圍越來越廣。移動部件上的雙核,桌面部件上的四核,服務器部件上的數十個核心。這將成為計算機性能的未來嗎?不幸的是。

Amdahl定律以IBM / 360的設計者Gene Amdahl命名,是一個公式,它給出了在固定工作負載下執行任務的延遲的理論加速,這可以預期資源得到改善的系統。

AmdahlsLaw

Amdahl定律告訴我們,程序的最大加速時間受程序的連續部分的限制。如果編寫一個程序,其95%的執行能夠並行運行,即使有數千個處理器,程序執行的最大加速也限制為20倍。

想想你每天工作的程序,他們的執行程序有多少是可以分開的?

1.9。動態優化

隨着時鍾速度的停滯以及在問題上拋出額外核心的回報有限,加速從何而來?它們來自芯片本身的架構改進。這些是具有Nehalem,Sandy Bridge和Skylake等名稱的五到七年大型項目

在過去二十年中,性能的大部分提升來自於體系結構的改進:

1.9.1。亂序執行

亂序,也稱為超標量,執行是一種從CPU執行的代碼中提取所謂的指令級並行性的方法。現代CPU在硬件級別有效地執行SSA以識別操作之間的數據依賴性,並且在可能的情況下並行地運行獨立指令。

但是,任何一段代碼中固有的並行數量都是有限的。它也非常耗電。大多數現代CPU已經確定每個核心有六個執行單元,因為在管道的每個階段都有一個n平方成本將每個執行單元連接到所有其他執行單元。

1.9.2。投機執行

保存最小的微控制器,所有CPU利用指令流水線重疊指令獲取/解碼/執行/提交周期中的部分。

800px Fivestagespipeline

指令流水線的問題是分支指令,平均每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的內存。

記憶差距

但是,處理器速度和內存訪問時間之間的差距仍在繼續增長。

BmBr2mwCIAAhJo1

但是,就等待內存而丟失的處理器周期而言,物理內存仍然遙不可及,因為內存跟不上CPU速度的增長。

因此,大多數現代處理器都受到內存延遲而非容量的限制。

1.12。緩存規則我周圍的一切

潛伏

幾十年來,處理器/內存上限的解決方案是添加一個緩存 - 一塊靠近CPU的小型快速內存,現在直接集成到CPU上。

但;

  • 幾十年來,L1一直停留在每核心32kb

  • L2在最大的英特爾部分上緩慢爬升至512kb

  • L3現在在4-32mb范圍內測量,但其訪問時間是可變的

E5v4blockdiagram

緩存的大小有限,因為它們在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

本講座的目的是說明當你談論程序或系統的性能完全在軟件中時。等待更快的硬件來挽救這一天是一個愚蠢的錯誤。

但有一個好消息,我們可以在軟件方面做出一些改進,這就是我們今天要討論的內容。

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 } }
  基准函數與_test.go文件中的測試一起存在

基准測試類似於測試,唯一真正的區別是他們需要的是一個*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
 

go test也會在匹配基准測試之前在包中運行所有測試,所以如果你在包中有很多測試,或者他們需要很長時間才能運行,你可以通過提供go test’s `-run一個沒有匹配的正則表達式的標志來排除它們即。

go test -run=^$

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。

 

所述-8后綴涉及的值GOMAXPROCS被用來運行該測試。此數字GOMAXPROCS默認為啟動時Go進程可見的CPU數。您可以使用-cpu標記更改此值,該標記采用值列表來運行基准測試。

% go test -bench=. -cpu=1,2,4 ./examples/fib/
goos: darwin
goarch: amd64
BenchmarkFib20             30000             39115 ns/op
BenchmarkFib20-2           30000             39468 ns/op
BenchmarkFib20-4           50000             40728 ns/op
PASS
ok      _/Users/dfc/devel/high-performance-go-workshop/examples/fib     5.531s

這顯示了使用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

嘗試使用-benchtime10x,20x,50x,100x和300x 運行上面的fib台你看到了什么?

  如果您發現go test需要針對特定​​軟件包調整適用的默認值,我建議將這些設置編成一個,Makefile以便每個想要運行基准測試的人都可以使用相同的設置進行編碼。

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可以幫助我們解決這個問題。

 

保存基准運行的輸出很有用,但您也可以保存生成它二進制文件這使您可以重新運行基准測試以前的迭代。為此,使用-c標志來保存測試二進制文件 - 我經常將此二進制文件重命名.test.golden

%go test -c
%mv fib.test fib.golden

先前的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) } }
  該文件還包括一個全面的測試Fib如果沒有驗證當前行為的測試,請不要嘗試改進基准測試。

為了比較我們的新版本,我們編譯了一個新的測試二進制文件並對它們進行基准測試並用於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個報告,即使您這樣做了-count=1010%或更低的拒絕率是可以的,高於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
 

您還可以使用該go test -benchmem標志強制測試框架報告所有運行的基准測試的分配統計信息。

% go test -run=^$ -bench=. -benchmem bufio
goos: darwin
goarch: amd64
pkg: bufio
BenchmarkReaderCopyOptimal-8            20000000                93.5 ns/op            16 B/op          1 allocs/op
BenchmarkReaderCopyUnoptimal-8          10000000               155 ns/op              32 B/op          2 allocs/op
BenchmarkReaderCopyNoWriteTo-8            500000              3238 ns/op           32800 B/op          3 allocs/op
BenchmarkReaderWriteToOptimal-8          5000000               335 ns/op              16 B/op          1 allocs/op
BenchmarkWriterCopyOptimal-8            20000000                96.7 ns/op            16 B/op          1 allocs/op
BenchmarkWriterCopyUnoptimal-8          10000000               124 ns/op              32 B/op          2 allocs/op
BenchmarkWriterCopyNoReadFrom-8           500000              3219 ns/op           32800 B/op          3 allocs/op
BenchmarkReaderEmpty-8                   2000000               748 ns/op            4224 B/op          3 allocs/op
BenchmarkWriterEmpty-8                   2000000               662 ns/op            4096 B/op          1 allocs/op
BenchmarkWriterFlush-8                  100000000               16.9 ns/op             0 B/op          0 allocs/op
PASS
ok      bufio   20.366s

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 }

這是確保編譯器無法優化循環體的推薦方法。

首先,我們通過存儲它來使用調用的結果其次,因為一旦基准結束,就在本地范圍內聲明,結果永遠不會被程序的另一部分看到,所以作為最終行為,我們將值賦給包公共變量popcntrrBenchmarkPopcntrrResult

因為Result是公共的,編譯器無法證明導入這個的另一個包將無法看到Result隨時間變化的值,因此它無法優化導致其賦值的任何操作。

如果我們Result直接分配會怎么樣?這會影響基准時間嗎?那么如果我們分配的結果popcnt_

  在我們之前的Fib基准測試中,如果我們這樣做,我們沒有采取這些預防措施?

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,內存和塊配置文件。

  • -cpuprofile=$FILE寫一個CPU配置文件$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

我們今天要討論的第一個工具是pprofpprof來自Google Perf Tools工具套件,並且自最早的公開發布以來已經集成到Go運行時中。

pprof 由兩部分組成:

  • runtime/pprof 每個Go程序都內置了一個包

  • 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一個sync.Mutex被另一個goroutine中鎖定。

塊分析是一種非常專業的工具,在您認為已消除所有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一段時間,你可能會被告知pprof有兩個參數。從Go 1.9開始,配置文件包含呈現配置文件所需的所有信息。您不再需要生成配置文件的二進制文件。🎉

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命令可視化此調用這將從配置文件數據生成有向圖。在幕后,它使用dotGraphviz 命令。

但是,在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 total mainreadbyte368.72kB (100%) 16B 368.72kB runtimemain0 of 368.72kB (100%) mainmain0 of 368.72kB (100%) 368.72kB 368.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%) 16B 43806 runtimemain0 of 43856 (99.88%) mainmain0 of 43856 (99.88%) 43856 mainallocate31 (0.071%)of 43837 (99.83%) 43806 43837

 

毫不奇怪,超過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 total runtimemalg24 (40.00%) 384B 24 runtimeallocm7 (11.67%)of 21 (35.00%) 7 1kB 7 runtimemcommoninit0 of 7 (11.67%) 7 runtimemstart0 of 17 (28.33%) runtimesystemstack3 (5.00%)of 14 (23.33%) 14 runtimemstart10 of 3 (5.00%) 3 runtimegcBgMarkWorker8 (13.33%) 16B 8 runtimeschedule0 of 18 (30.00%) runtimeresetspinning0 of 15 (25.00%) 15 runtimestoplockedm0 of 3 (5.00%) 3 profileStartfunc82 (3.33%)of 9 (15.00%) 16B 1 96B 1 signalNotify3 (5.00%)of 7 (11.67%) 7 runtimemcall0 of 15 (25.00%) runtimepark_m0 of 15 (25.00%) 15 64B 2 48B 1 runtimenewprocfunc10 of 11 (18.33%) 11 runtimenewm0 of 21 (35.00%) 21 runtimeensureSigMfunc10 of 5 (8.33%) runtimeLockOSThread0 of 3 (5.00%) 3 runtimechansend10 of 1 (1.67%) 1 runtimeselectgo0 of 1 (1.67%) 1 runtimestartm0 of 18 (30.00%) 18 144B 1 16B 1 48B 1 signalNotifyfunc10 of 4 (6.67%) 4 signalsignal_enable4 (6.67%) 96B 4 runtimeacquireSudog2 (3.33%) 96B 2 runtimemain0 of 6 (10.00%) mainmain0 of 6 (10.00%) 6 profileStart1 (1.67%)of 5 (8.33%) 48B 1 profileStartfunc20 of 4 (6.67%) 4 log(*Logger)Output1 (1.67%)of 4 (6.67%) 144B 1 log(*Logger)formatHeader0 of 3 (5.00%) 3 runtimenewproc10 of 11 (18.33%) 10 runtimeallgadd1 (1.67%) 1 timeLoadLocationFromTZData2 (3.33%)of 3 (5.00%) 224B 1 4kB 1 timebyteString1 (1.67%) 1 5 mainallocate0 of 1 (1.67%) 1 mainmakeByteSlice1 (1.67%) 16B 1 128B 1 16B 1 logPrintf0 of 4 (6.67%) 4 timeTimeDate0 of 3 (5.00%) 3 4 1 signalenableSignal0 of 4 (6.67%) 4 4 runtimestartTemplateThread0 of 3 (5.00%) 3 runtimechansend0 of 1 (1.67%) 1 1 runtimehandoffp0 of 3 (5.00%) 3 runtimempreinit0 of 7 (11.67%) 7 7 3 11 15 runtimewakep0 of 15 (25.00%) 15 1 3 3 15 sync(*Once)Do0 of 3 (5.00%) timeinitLocal0 of 3 (5.00%) 3 time(*Location)get0 of 3 (5.00%) 3 Timedate0 of 3 (5.00%) 3 Timeabs0 of 3 (5.00%) 3 3 timeloadLocation0 of 3 (5.00%) 3 3

 

我們看到的不是在配置文件期間分配的對象,而是在獲取配置文件時仍在使用的對象- 這忽略了垃圾收集器已回收的對象的堆棧跟蹤。

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.94s testingrunBenchmarksfunc10 of 3.11s (39.80%) 3.11s runtimechanrecv13.23s (41.25%) runtimemain0 of 3.11s (39.80%) mainmain0 of 3.11s (39.80%) 3.11s http(*persistConn)writeLoop0 of 2.54s (32.44%) 2.54s testing(*B)launch0 of 1.94s (24.82%) 1.94s ioutilReadAll0 of 0.11s (1.46%) 0.11s httpGet0 of 1.83s (23.37%) 1.83s http(*persistConn)readLoop0 of 0.18s (2.36%) 0.18s sync(*Cond)Wait0.04s (0.57%) http(*conn)serve0 of 0.04s (0.57%) http(*response)finishRequest0 of 0.04s (0.57%) 0.04s testing(*B)Run0 of 3.11s (39.80%) testing(*B)run0 of 3.11s (39.77%) 3.11s http(*Transport)roundTrip0 of 1.83s (23.37%) http(*persistConn)roundTrip0 of 1.83s (23.36%) 1.83s bytes(*Buffer)ReadFrom0 of 0.11s (1.46%) http(*bodyEOFSignal)Read0 of 0.11s (1.46%) 0.11s ioutilreadAll0 of 0.11s (1.46%) 0.11s 0.11s http_testTestMain0 of 3.11s (39.80%) 3.11s http(*Client)Do0 of 1.83s (23.37%) http(*Client)do0 of 1.83s (23.37%) 1.83s http(*Client)Get0 of 1.83s (23.37%) 1.83s http(*Client)send0 of 1.83s (23.37%) 1.83s httpsend0 of 1.83s (23.37%) 1.83s http(*Transport)RoundTrip0 of 1.83s (23.37%) 1.83s http(*bodyEOFSignal)condfn0 of 0.11s (1.46%) 0.11s http(*persistConn)readLoopfunc40 of 0.11s (1.46%) 0.11s http(*connReader)abortPendingRead0 of 0.04s (0.57%) 0.04s 0.11s 1.83s 0.04s 1.83s 1.83s testing(*M)Run0 of 3.11s (39.80%) 3.11s testing(*B)doBench0 of 3.11s (39.77%) 3.11s testing(*benchContext)processBench0 of 3.11s (39.77%) 3.11s testingrunBenchmarks0 of 3.11s (39.80%) 3.11s 3.11s 3.11s 3.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程序的演示文稿。

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個整數,而不是堆。不需要垃圾回收numbersSum返回時會自動釋放

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.Xp.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,告訴我們它可以內聯。

  • 第二個是報告Max第12行的內容已被內聯到呼叫者。

使用//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?

輸出-S不是進入二進制文件的最終機器代碼。鏈接器在最后的鏈接階段進行一些處理。類似FUNCDATAPCDATA是垃圾收集器的元數據,在鏈接時移動到其他位置。如果你正在讀取輸出-S,只需忽略FUNCDATAPCDATA行; 它們不是最終二進制文件的一部分。

4.3.3。討論

為什么我聲明a,並bF()為常數?

嘗試輸出如果ab被聲明為變量會發生什么如果a作為參數b傳遞會發生什么F()

  -gcflags=-S不會阻止在工作目錄中構建最終二進制文件。如果發現后續運行go build …​沒有產生輸出,請刪除./max工作目錄中二進制文件。

4.3.4。調整內聯級別

使用標志執行調整內聯級別-gcflags=-l有些令人困惑的傳遞單個-l將禁用內聯,兩個或更多將啟用內聯更積極的設置。

  • -gcflags=-l,內聯禁用。

  • 沒事,定期內聯。

  • -gcflags='-l -l' 內聯級別2,更具侵略性,可能更快,可能會制作更大的二進制文件。

  • -gcflags='-l -l -l' 內聯級別3,再次更具侵略性,二進制文件肯定更大,可能更快,但也可能是錯誤的。

  • -gcflags=-l=4 Go 1.11中的四個“-l`s”將啟用實驗性中間堆棧內聯優化

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,顯示了這種技術適用於sync.RWMutex.Unlock()

4.4。死代碼消除

為什么重要的是ab是常數?

要了解發生了什么事讓我們來看看編譯器看到一旦其內聯什么MaxF我們無法輕易地從編譯器中獲得這一點,但是它可以直接手動完成。

之前:

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.5。編譯器標志練習

編譯器標志提供:

go build -gcflags=$FLAGS

調查以下編譯器函數的操作:

  • -S打印正在編譯的(Go flavor)程序集。

  • -l控制內襯的行為; -l禁用內聯,-l -l增加它(更多-l會增加編譯器對內聯代碼的興趣)。試驗編譯時間,程序大小和運行時間的差異。

  • -m控制優化決策的打印,如內聯,逃逸分析。-m-m`打印出有關編譯器思考內容的更多細節。

  • -l -N 禁用所有優化。

  如果發現后續運行go build …​沒有產生輸出,請刪除./max工作目錄中二進制文件。

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
  不要使用time go run mandebrot.go或者你需要花費多長時間來編譯程序以及運行程序。

5.1.2。該計划在做什么?

因此,在這個例子中,程序用1.6秒生成mandelbrot並寫入png。

這樣好嗎?我們可以加快速度嗎?

回答這個問題的一種方法是使用Go的內置pprof支持來分析程序。

我們試試吧。

5.2。生成配置文件

要生成配置文件,我們需要

  1. runtime/pprof直接使用包。

  2. 使用包裝器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
  我們可以go run在這種情況下使用,因為cpu配置文件只包含執行mandelbrot.go,而不包括其編譯。

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
  使用pkg/profile不是強制性的,但它會收集很多關於收集和記錄痕跡的樣板,因此我們將在本次研討會的其余部分使用它。

5.3.2。分析個人資料

現在我們有了一個配置文件,我們可以go tool pprof用來分析它。

% go tool pprof -http=:8080 cpu.pprof

在這次運行中,我們看到程序運行了1.81秒(分析增加了一小部分開銷)。我們還可以看到pprof僅捕獲數據1.53秒,因為pprof是基於樣本的,依賴於操作系統的SIGPROF計時器。

  從Go 1.9開始,pprof跟蹤包含分析跟蹤所需的所有信息。您不再需要也具有生成跟蹤的匹配二進制文件。🎉

我們可以使用toppprof函數對跟蹤記錄的函數進行排序

% 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實際上正在完成大部分工作。

 

您還可以使用web命令可視化配置文件,如下所示:

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 total mainpaintmandelbrot.go1s (65.36%) runtimemainproc.go0 of 1.53s (100%) mainmainmandelbrot.go0 of 1.53s (100%) 1.53s mainfillPixelmandelbrot.go0.27s (17.65%)of 1.27s (83.01%) 1s(inline) image/pngEncodewriter.go0 of 0.26s (16.99%) 0.26s mainseqFillImgmandelbrot.go0 of 1.27s (83.01%) 1.27s runtimemallocgcmalloc.go0.13s (8.50%)of 0.16s (10.46%) runtime(*mcache)nextFreemalloc.go0 of 0.03s (1.96%) 0.03s image/png(*encoder)writeImagewriter.go0 of 0.19s (12.42%) main(*img)Atmandelbrot.go0 of 0.18s (11.76%) 0.11s image/pngfilterwriter.go0.01s (0.65%) 0.01s compress/zlib(*Writer)Writewriter.go0 of 0.07s (4.58%) 0.07s image/png(*Encoder)Encodewriter.go0 of 0.26s (16.99%) image/png(*encoder)writeIDATswriter.go0 of 0.19s (12.42%) 0.19s image/pngopaquewriter.go0 of 0.07s (4.58%) 0.07s runtimeconvT2Inoptriface.go0 of 0.18s (11.76%) 0.18s syscallSyscallasm_darwin_amd64.s0.05s (3.27%) 0.16s runtimememmovememmove_amd64.s0.02s (1.31%) 0.02s compress/flate(*compressor)deflatedeflate.go0.01s (0.65%)of 0.07s (4.58%) compress/flate(*compressor)findMatchdeflate.go0 of 0.01s (0.65%) 0.01s compress/flate(*compressor)writeBlockdeflate.go0 of 0.05s (3.27%) 0.05s runtimemmapsys_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.05s compress/flate(*huffmanBitWriter)writeTokenshuffman_bit_writer.go0 of 0.05s (3.27%) compress/flate(*huffmanBitWriter)writeBitshuffman_bit_writer.go0 of 0.01s (0.65%) 0.01s compress/flate(*huffmanBitWriter)writeCodehuffman_bit_writer.go0 of 0.04s (2.61%) 0.04s runtimesystemstackasm_amd64.s0 of 0.03s (1.96%) runtime(*mcache)nextFreefunc1malloc.go0 of 0.02s (1.31%) 0.02s runtime(*mheap)allocfunc1mheap.go0 of 0.01s (0.65%) 0.01s compress/flatematchLendeflate.go0.01s (0.65%) runtime(*mcentral)growmcentral.go0 of 0.02s (1.31%) runtime(*mheap)allocmheap.go0 of 0.01s (0.65%) 0.01s runtimeheapBitsinitSpanmbitmap.go0 of 0.01s (0.65%) 0.01s runtimememclrNoHeapPointersmemclr_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.05s bufio(*Writer)Writebufio.go0 of 0.05s (3.27%) 0.05s compress/flate(*Writer)Writedeflate.go0 of 0.07s (4.58%) compress/flate(*compressor)writedeflate.go0 of 0.07s (4.58%) 0.07s 0.01s 0.07s compress/flate(*huffmanBitWriter)writeBlockhuffman_bit_writer.go0 of 0.05s (3.27%) 0.05s 0.05s 0.01s 0.05s 0.04s 0.07s image/png(*encoder)writeChunkwriter.go0 of 0.05s (3.27%) 0.05s os(*File)Writefile.go0 of 0.05s (3.27%) 0.05s 0.19s 0.26s 0.07s internal/poll(*FD)Writefd_unix.go0 of 0.05s (3.27%) syscallWritesyscall_unix.go0 of 0.05s (3.27%) 0.05s 1.27s os(*File)writefile_unix.go0 of 0.05s (3.27%) 0.05s 0.05s 0.03s runtime(*mcache)refillmcache.go0 of 0.02s (1.31%) 0.02s runtime(*mcentral)cacheSpanmcentral.go0 of 0.02s (1.31%) 0.02s 0.02s 0.01s runtime(*mheap)alloc_mmheap.go0 of 0.01s (0.65%) 0.01s runtime(*mheap)allocSpanLockedmheap.go0 of 0.01s (0.65%) runtime(*mheap)growmheap.go0 of 0.01s (0.65%) 0.01s 0.01s runtime(*mheap)sysAllocmalloc.go0 of 0.01s (0.65%) 0.01s runtimesysMapmem_darwin.go0 of 0.01s (0.65%) 0.01s runtimenewMarkBitsmheap.go0 of 0.01s (0.65%) 0.01s runtimenewArenaMayUnlockmheap.go0 of 0.01s (0.65%) runtimesysAllocmem_darwin.go0 of 0.01s (0.65%) 0.01s 0.01s 0.01s 0.01s syscallwritezsyscall_darwin_amd64.go0 of 0.05s (3.27%) 0.05s 0.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文件。這會在堆上生成垃圾,因此跟蹤在此時發生變化,我們可以看到垃圾收集堆的經典鋸齒模式。

跟蹤配置文件提供低至微秒級別的定時分辨率這是您通過外部分析無法獲得的。

 
去工具痕跡

在我們繼續之前,我們應該談談跟蹤工具的使用。

  • 該工具使用Chrome內置的javascript調試支持。跟蹤配置文件只能在Chrome中查看,它們無法在Firefox,Safari,IE / Edge中使用。抱歉。

  • 因為這是Google產品,所以它支持鍵盤快捷鍵; 用於WASD導航,用於?獲取列表。

  • 查看跟蹤會占用大量內存。說真的,4Gb不會削減它,8Gb可能是最小的,更多肯定更好。

  • 如果你從像Fedora這樣的OS發行版安裝了Go,那么跟蹤查看器的支持文件可能不是主golangdeb / rpm的一部分,它們可能在某個-extra包中。

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的改進。

  • 放大我們看到onePerRowFillImg運行時間更長,並且由於goroutine 生產工作提前完成,調度程序有效地通過剩余的可運行的goroutine。

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/

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開始,這包括跟蹤分析器。

  net/http/pprof注冊http.DefaultServeMux如果您ServeMux隱式或明確地使用它,您可能會無意中將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

並發素篩是寫入的第一個圍棋程序之一。

讓我們看一下使用執行跟蹤器的操作。

5.9.6。更多資源

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:顯示godocGODEBUG=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

 

net/http/pprof將使用您的默認值注冊自己http.ServeMux

請注意,如果您使用,這將是可見的http.ListenAndServe(address, nil)

演示:godoc -http=:8080,顯示/debug/pprof

6.3.1。垃圾收集器調整

Go運行時提供了一個環境變量來調整GC GOGC

GOGC的公式是

lrlë ⋅  1 G ^ Ö ģ Ç100GØ一個=[RË一個CH一個bË1+GØGC100

例如,如果我們當前有256MB堆,並且GOGC=100(默認值),當堆填滿時,它將增長到

512 256 M.⋅  1 100100512中號=256中號1+100100

  • GOGC大於100的會使堆增長更快,從而降低GC的壓力。

  • GOGC小於100的會導致堆緩慢增長,從而增加GC的壓力。

默認值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軟件包包含許多相同的操作- ,  SplitCompareHasPrefixTrim等-作為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 }
 

sync.Pool不是緩存。它可以並且將在at_any_time清空

不要將重要物品放入sync.Pool,它們將被丟棄。

 

在每個GC上清空自己的sync.Pool的設計可能會在Go 1.13中改變,這將有助於提高其效用。

此CL通過引入受害者緩存機制來解決此問題。不會清除池,而是刪除受害者緩存,並將主緩存移動到受害者緩存。因此,在穩定狀態下,(大致)沒有新的分配,但如果池使用率下降,則仍將在兩個GC(而不是一個)中收集對象。 - 奧斯汀克萊門茨

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.2。Go對某些請求使用高效的網絡輪詢

Go運行時使用高效的操作系統輪詢機制(kqueue,epoll,windows IOCP等)處理網絡IO。許多等待的goroutine將由單個操作系統線程提供服務。

但是,對於本地文件IO,Go不實現任何IO輪詢。a上的每個操作*os.File在進行時消耗一個操作系統線程。

大量使用本地文件IO會導致程序產生數百或數千個線程; 可能超過您的操作系統允許。

您的磁盤子系統不希望能夠處理數百或數千個並發IO請求。

 

要限制並發阻塞IO的數量,請使用worker goroutines池或緩沖通道作為信號量。

var semaphore = make(chan struct{}, 10) func processRequest(work *Work) { semaphore <- struct{}{} // acquire semaphore // process request <-semaphore // release semaphore }

7.3。注意應用程序中的IO乘數

如果您正在編寫服務器進程,那么它的主要工作是復用通過網絡連接的客戶端以及存儲在應用程序中的數據。

大多數服務器程序接受請求,進行一些處理,然后返回結果。這聽起來很簡單,但根據結果,它可以讓客戶端在服務器上消耗大量(可能無限制)的資源。以下是一些需要注意的事項:

  • 每個傳入請求的IO請求數量; 單個客戶端請求生成多少個IO事件?如果從緩存中提供多個請求,則它可能平均為1,或者可能小於1。

  • 服務查詢所需的讀取量; 它是固定的,N + 1還是線性的(讀取整個表格以生成結果的最后一頁)。

如果內存很慢,相對來說,那么IO太慢了,你應該不惜一切代價避免這樣做。最重要的是避免在請求的上下文中執行IO - 不要讓用戶等待磁盤子系統寫入磁盤,甚至不要讀取。

7.4。使用流式IO接口

盡可能避免將數據讀入[]byte並傳遞給它。

根據請求,您最終可能會將兆字節(或更多!)的數據讀入內存。這給GC帶來了巨大壓力,這將增加應用程序的平均延遲。

而是使用io.Readerio.Writer構造處理管道來限制每個請求使用的內存量。

為了提高效率,請考慮實施io.ReaderFromio.WriterTo如果您使用了很多io.Copy這些接口更有效,並避免將內存復制到臨時緩沖區。

7.5。超時,超時,超時

在不知道最長時間的情況下,切勿啟動IO操作。

您需要設置超時你讓每網絡請求SetDeadlineSetReadDeadlineSetWriteDeadline

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代碼?

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.10。討論

任何問題?

最后的問題和結論

可讀性意味着可靠 - Rob Pike

從最簡單的代碼開始。

測量描述您的代碼以識別瓶頸,不要猜測

如果表現良好,請停止您不需要優化所有內容,只需要優化代碼中最熱門的部分。

隨着應用程序的增長或流量模式的發展,性能熱點將會發生變化。

不要留下對性能不重要的復雜代碼,如果瓶頸移到其他地方,則用更簡單的操作重寫它。

總是編寫最簡單的代碼,編譯器針對普通代碼進行了優化

更短的代碼是更快的代碼; Go不是C ++,不要指望編譯器解開復雜的抽象。

更短的代碼是更小的代碼; 這對CPU的緩存很重要。

密切關注分配,盡可能避免不必要的分配。

如果他們不必正確,我可以把事情做得很快。 - 拉斯考克斯

性能和可靠性同樣重要。

我認為制作速度非常快的服務器,定期出現恐慌,死鎖或OOM的價值不大。

不要為了可靠性而交易性能。


1 Hennessy等人:超過40年的1.4倍年度績效改進。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM