幾周前,我讀了一篇名爲「 Good Code vs Go Code中的錯誤代碼 」的文章,做者指導咱們逐步完成實際業務用例的重構。html
本文的重點是將「壞代碼」轉變爲「良好代碼」:更具慣用性,更易讀,利用go語言的細節。但它也堅持將性能做爲項目的一個重要方面。這引起了個人好奇心:讓咱們深刻挖掘!git
幾周前,我讀了一篇名爲「 Good Code vs Go Code中的錯誤代碼 」的文章,做者指導咱們逐步完成實際業務用例的重構。html
本文的重點是將「壞代碼」轉變爲「良好代碼」:更具慣用性,更易讀,利用go語言的細節。但它也堅持將性能做爲項目的一個重要方面。這引起了個人好奇心:讓咱們深刻挖掘!git
該程序基本上讀取一個輸入文件,並解析每一行以填充內存中的對象。github
$ go test -bench =。
所以,在個人機器上,「好代碼」的速度提升了16%。咱們能得到更多嗎?golang
根據個人經驗,代碼質量和性能之間存在有趣的關聯。當您成功地重構代碼以使其更清晰且更加分離時,您一般最終會使其更快,由於它不會使以前執行的無關指令變得混亂,而且還由於一些可能的優化變得明顯且易於實現。正則表達式
另外一方面,若是你進一步追求性能,你將不得不放棄簡單並訴諸於黑客。你確實會刮掉幾毫秒,但代碼質量會受到影響,由於它會變得更難以閱讀和推理,更脆弱,更不靈活。算法
這是一個權衡:你願意走多遠?數據庫
爲了正確肯定您的績效工做的優先順序,最有價值的策略是肯定您的瓶頸並專一於它們。要實現這一點,請使用分析工具!Pprof和Trace是你的朋友:canvas
$ go test -bench =。-cpuprofile cpu.prof
$ go tool pprof -svg cpu.prof> cpu.svg
$ go test -bench =。-trace trace.out
$ go工具跟蹤trace.out
跟蹤證實使用了全部CPU內核(底線0,1等),這在一開始看起來是件好事。但它顯示了數千個小的彩色計算切片,以及一些空閒插槽,其中一些核心處於空閒狀態。咱們放大:緩存
每一個核心實際上花費大量時間閒置,並在微任務之間保持切換。看起來任務的粒度不是最優的,致使許多上下文切換以及因爲同步而致使的爭用。網絡
讓咱們檢查一下競爭檢測器是否同步是正確的(若是沒有,那麼咱們的問題比性能更大):
$ go test -race
PASS
是!!看起來是正確的,沒有遇到數據爭用狀況。測試函數和基準函數是不一樣的(參見文檔),但在這裏他們調用相同的函數ParseAdexpMessage,咱們可使用-race
。
「好」版本中的併發策略包括在其本身的goroutine中處理每行輸入,以利用多個核心。這是一種合法的直覺,由於goroutines的聲譽是輕量級和廉價的。咱們多少得益於併發性?讓咱們與單個順序goroutine中的相同代碼進行比較(只需刪除行解析函數調用以前的go關鍵字)
哎呀,沒有任何並行性,它實際上更快。這意味着啓動goroutine的(非零)開銷超過了同時使用多個核心所節省的時間。
天然的下一步,由於咱們如今順序而不是同時處理行,是爲了不使用結果通道的(非零)開銷:讓咱們用裸片替換它。
咱們如今從「好」版本得到了大約40%的加速,只是簡化了代碼,刪除了併發(差別)。
如今讓咱們看一下Pprof圖中的熱函數調用:
咱們當前版本的基準(順序,帶切片)花費86%的時間實際解析消息,這很好。咱們很快注意到,總時間的43%用於將正則表達式與(* Regexp).FindAll匹配 。
雖然regexp是從原始文本中提取數據的一種方便靈活的方法,但它們存在缺陷,包括內存和運行時的成本。它們很強大,但對於許多用例來講可能有點過度。
在咱們的程序中,模式
patternSubfield =「 - 。[^ - ] *」
主要用於識別以短劃線「 - 」 開頭的「 命令 」,而且一行可能有多個命令。經過一些調整,可使用bytes.Split完成。讓咱們調整代碼(commit,commit)以使用Split替換regexp:
哇,這是40%的額外增益!
CPU圖如今看起來像這樣:
沒有更多正則表達式的巨大成本。從5個不一樣的功能中分配內存花費了至關多的時間(40%)。有趣的是,總時間的21%如今由字節佔.Trim 。
bytes.Trim指望一個「 cutset string」做爲參數(對於要在左側和右側刪除的字符),但咱們僅使用單個空格字節做爲cutset。這是一個例子,您能夠經過引入一些複雜性來得到性能:實現您本身的自定義「trim」函數來代替標準庫函數。在自定義的「微調」的交易,只有一個割集字節。
是的,另外20%被削減了。當前版本的速度是原始「壞」速度的4倍,而機器只使用1個CPU內核。至關實質!
以前咱們放棄了在線處理級別的併發性,可是經過併發更新仍然存在改進的空間,而且具備更粗略的粒度。例如,在每一個文件在其本身的goroutine中處理時,處理6,000個文件(6,000條消息)在個人工做站上更快:
66%的勝利(即3倍的加速),這是好的但「不是那麼多」,由於它利用了我全部的12個CPU內核!這可能意味着使用新的優化代碼,處理整個文件仍然是一個「小任務」,goroutine和同步的開銷不可忽略不計。
有趣的是,將消息數量從6,000增長到120,000對順序版本的性能沒有影響,而且下降了「每一個消息的1個goroutine」版本的性能。這是由於啓動大量的goroutine是可能的,有時是有用的,但它確實給go運行時調度程序帶來了一些壓力。
咱們能夠經過僅建立少數工做人員來減小執行時間(不是12倍因素,但仍然是這樣),例如12個長期運行的goroutine,每一個goroutine處理一部分消息:
與順序版本相比,大批消息的調優併發性刪除了79%的執行時間。請注意,只有在確實要處理大量文件時,此策略纔有意義。
全部CPU核心的最佳利用包括幾個goroutine,每一個goroutine處理至關數量的數據,在完成以前沒有任何通訊和同步。
選擇與可用CPU核心數相等的多個進程(goroutine)是一種常見的啓發式方法,但並不老是最佳:您的里程可能會根據任務的性質而有所不一樣。例如,若是您的任務從文件系統讀取或發出網絡請求,那麼性能比CPU核心具備更多的goroutine是徹底合理的。
咱們已經達到了這樣的程度,即經過本地化加強很難提升解析代碼的效率。如今,執行時間由小對象(例如Message結構)的分配和垃圾收集主導,這是有道理的,由於已知內存管理操做相對較慢。進一步優化分配策略......留給狡猾的讀者練習。
這就是今天,我但願你喜歡這個旅程。如下是一些免責聲明和外賣: