改變一個字元讓Go程式快42%
codeowners是一個 Go 程式,它根據GitHubCODEOWNERS檔案中定義的一組規則列印出儲存庫中每個檔案的所有者。
在考慮給定路徑時,最後匹配的規則獲勝:一個簡單但幼稚的匹配演算法透過每條路徑的規則向後迭代,在找到匹配項時停止:
type Ruleset Rule func (r Ruleset) Match(path string) (*Rule, error) { for i := len(r) - 1; i >= 0; i-- { rule := r match, err := rule.Match(path) if match || err != nil { return &rule, err } } return nil, nil } |
使用 pprof 和火焰圖查詢瓶頸
上述這個工具在針對中等大型儲存庫執行時有點慢,為了檢視程式將時間花在哪裡,我使用 pprof 記錄了一個 CPU 配置檔案。您可以透過將此程式碼段新增到main函式頂部來獲取生成的 CPU 配置檔案:
pprofFile, pprofErr := os.Create("cpu.pprof") if pprofErr != nil { log.Fatal(pprofErr) } pprof.StartCPUProfile(pprofFile) defer pprof.StopCPUProfile() |
我經常使用 pprof,所以我將該程式碼儲存為vscode 片段。我只需鍵入pprof,點選選項卡,就會出現該片段。
Go 帶有一個方便的互動式配置檔案視覺化工具。我透過執行以下命令將配置檔案視覺化為火焰圖,然後導航到頁面頂部選單中的火焰圖檢視。
$ go tool pprof -http=":8000" ./codeowners ./cpu.pprof |
正如我所料,大部分時間都花在了那個Match功能上。CODEOWNERS 模式被編譯為正規表示式,Match函式的大部分時間花在了 Go 的正規表示式引擎上。但我也注意到很多時間花在分配和回收記憶體上。
下面火焰圖中的紫色塊與模式匹配gc|malloc,您可以看到它們總體上代表了程式執行時間的重要部分。
使用逃逸分析跟蹤尋找堆分配
那麼就要看看是否有任何堆分配我們可以擺脫以減少 GC 壓力和花費在malloc.
Go 編譯器使用一種稱為逃逸分析的技術來確定何時需要將一些記憶體駐留在堆上:
假設一個函式初始化一個結構然後返回一個指向它的指標。如果結構是在堆疊上分配的,那麼一旦函式返回並且相應的堆疊幀失效,返回的指標就會失效。
在這種情況下,Go 編譯器會確定指標已經“逃逸”了函式,並將結構移至堆中。
您可以在build構建時,透過傳遞-gcflags=-m來檢視:
$ go build -gcflags=-m *.go 2>&1 | grep codeowners.go ./codeowners.go:82:18: inlining call to os.IsNotExist ./codeowners.go:71:28: inlining call to filepath.Join ./codeowners.go:52:19: inlining call to os.Open ./codeowners.go:131:6: can inline Rule.Match ./codeowners.go:108:27: inlining call to Rule.Match ./codeowners.go:126:6: can inline Rule.RawPattern ./codeowners.go:155:6: can inline Owner.String ./codeowners.go:92:29: ... argument does not escape ./codeowners.go:96:33: string(output) escapes to heap ./codeowners.go:80:17: leaking param: path ./codeowners.go:70:31: string{...} does not escape ./codeowners.go:71:28: ... argument does not escape ./codeowners.go:51:15: leaking param: path ./codeowners.go:105:7: leaking param content: r ./codeowners.go:105:24: leaking param: path ./codeowners.go:107:3: moved to heap: rule ./codeowners.go:126:7: leaking param: r to result ~r0 level=0 ./codeowners.go:131:7: leaking param: r ./codeowners.go:131:21: leaking param: path ./codeowners.go:155:7: leaking param: o to result ~r0 level=0 ./codeowners.go:159:13: "@" + o.Value escapes to heap |
由於我們正在尋找heap堆分配,"moved to heap "是我們應該關注的短語。
回顧上面的Match程式碼,規則結構被儲存在Ruleset片中,我們可以確信它已經在堆中了。由於返回的是一個指向規則的指標,因此不需要額外的分配。然後我看到了:透過分配 rule := 【i】,我們將堆heap中分配的 Rule 從堆片斷中複製到棧stack中,然後再透過返回 &rule,我們建立了一個指向該結構副本的(逃避)指標。
幸運的是,解決這個問題很容易。我們只需要將“&”往上移一點,這樣我們就可以在片斷中獲取一個結構的引用,而不是複製它:
func (r Ruleset) Match(path string) (*Rule, error) { for i := len(r) - 1; i >= 0; i-- { - rule := r + rule := &r match, err := rule.Match(path) if match || err != nil { - return &rule, err + return rule, err } } return nil, nil } |
我確實考慮過另外兩種方法:
- 將Ruleset從Rule改為*Rule,這將意味著我們不再需要明確地獲取對規則的引用。
- 返回一個Rule而不是*Rule。這仍然會複製規則,但它應該留在堆疊stack中,而不是移動到堆heap中。
[然而,這兩個方法都會導致一個破壞性的改變,因為這個方法是公共API的一部分。
總之,在做了這個改變之後,我們可以透過從編譯器中得到一個新的跟蹤,並與舊的跟蹤進行比較,來看看它是否達到了預期效果。
$ diff trace-a trace-b 14a15 > ./codeowners.go:105:7: leaking param: r to result ~r0 level=0 16d16 < ./codeowners.go:107:3: moved to heap: rule |
成功!分配沒了。現在讓我們看看刪除一個堆分配如何影響效能:
$ hyperfine ./codeowners-a ./codeowners-b Benchmark 1: ./codeowners-a Time (mean ± σ): 4.146 s ± 0.003 s [User: 5.809 s, System: 0.249 s] Range (min … max): 4.139 s … 4.149 s 10 runs Benchmark 2: ./codeowners-b Time (mean ± σ): 2.435 s ± 0.029 s [User: 2.424 s, System: 0.026 s] Range (min … max): 2.413 s … 2.516 s 10 runs Summary ./codeowners-b ran 1.70 ± 0.02 times faster than ./codeowners-a |
由於該分配是針對匹配的每條路徑進行的,因此在這種情況下,刪除它會使速度提高 1.7 倍(意味著它的執行速度提高了 42%)。
相關文章
- 一個屌絲程式猿的人生(42)
- 讓PHOTOSHOP執行速度變快的10個技巧
- 關於SAP ABAP字元變數和字串變數字元個數的一個知識點,和一個血案字元變數字串
- 別讓自己變為一個廢掉的程式猿
- 通過設計讓APP變快的6個方法APP
- 程式碼改變世界 | 如何封裝一個簡單的 Koa封裝
- 風變程式設計Python課程:讓改變從現在開始程式設計Python
- 一勞永逸讓VB自動改變控制元件大小控制元件
- 推薦一個可以讓 go 程式跨平臺簡單部署的包Go
- 10g 新特性 快改變跟蹤
- 學習風變程式設計,讓我的人生多了一個可能!程式設計
- 大資料正在改變每一個行業大資料行業
- Tkinter (42) 控制變數變數
- 超實用!如何利用設計讓等待變得「快一點」?
- 8個小技巧,讓一個遊戲變得更好玩遊戲
- 在一個元素上:hover,改變另一個元素的css屬性CSS
- 如何保證一個類中的例項變數不被改變變數
- 一個徹底改變Redux的簡潔設計Redux
- 改變一個狀況不佳的專案組(轉)
- [一分鐘知識]改變無法改變的Query 變數變數
- 【小心】快停止這6種讓Python程式變慢的壞習慣!Python
- MagicArray:像php一樣,讓Go業務程式碼不再卷!PHPGo
- Linux:改變世界的一次程式碼提交Linux
- js 字串中取得第一個字元和最後一個字元JS字串字元
- Qt 程式改變文字大小QT
- 一個檔案的內容變成一個 go 語言的變數的小工具Go變數
- Go 語言的 4 個特性改動Go
- 蘋果新品來襲,快來看看都有哪些改變吧蘋果
- 求助掘金: 如何改變一個團隊的文化/氛圍
- 一個unusable 的索引REBUILD後分配的block是否改變索引RebuildBloC
- 把一個python程式改寫成JuliaPython
- 把一個Python程式改寫為JuliaPython
- 改變程式設計師開發方式的15個技術程式設計師
- 改變無法改變的Query 變數變數
- 一個gRPC-go範例程式RPCGo
- vue中data改變後,如何讓檢視同步更新Vue
- Apple Watch改變不了世界 蘋果讓人失望了APP蘋果
- gmcache一個用go寫的分散式快取,類似memcachedGo分散式快取