改變一個字元讓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%)。
相關文章
- 一個改變世界的“箱子”
- 關於SAP ABAP字元變數和字串變數字元個數的一個知識點,和一個血案字元變數字串
- 別讓自己變為一個廢掉的程式猿
- 程式碼改變世界 | 如何封裝一個簡單的 Koa封裝
- 風變程式設計Python課程:讓改變從現在開始程式設計Python
- 推薦一個可以讓 go 程式跨平臺簡單部署的包Go
- 學習風變程式設計,讓我的人生多了一個可能!程式設計
- Linux:改變世界的一次程式碼提交Linux
- Tkinter (42) 控制變數變數
- 【小心】快停止這6種讓Python程式變慢的壞習慣!Python
- 在一個元素上:hover,改變另一個元素的css屬性CSS
- 一個徹底改變Redux的簡潔設計Redux
- 8個小技巧,讓一個遊戲變得更好玩遊戲
- 20行Python程式碼教你讓影片字元化Python字元
- 字元流中第一個不重複的字元字元
- 求助掘金: 如何改變一個團隊的文化/氛圍
- 2000年,一個美國男人改變了法國育碧
- 一個檔案的內容變成一個 go 語言的變數的小工具Go變數
- Go 語言的 4 個特性改動Go
- 一個有趣的外掛,讓寫程式碼變成打怪升級的遊戲遊戲
- vue中data改變後,如何讓檢視同步更新Vue
- 驚了!goto 語句讓 Go 程式碼變成義大利麵條嗎?Go
- MagicArray:像php一樣,讓Go業務程式碼不再卷!PHPGo
- 得到字串 位元組 長度 中文 兩個字元 英文一個字元字串字元
- bgo: 讓構建 go 程式更容易Go
- 蘋果新品來襲,快來看看都有哪些改變吧蘋果
- 雲原生實戰派:2021 讓改變發生,2022 讓創新升級
- 怎樣改變小程式的首頁?
- “AI+山石網科” 讓安全發生“智“的改變!AI
- win10怎麼讓電腦變快_如何讓電腦執行速度快win10Win10
- 《快學 Go 語言》第 2 課 —— 變數基礎Go變數
- 時間可以改變一切
- 第一個mybatis程式,實現增刪改查CRUDMyBatis
- 第42篇 字元與進位制之間的轉換字元
- 微信小程式改變 button disabled 樣式微信小程式
- 劍指Offer 字元流中第一個不重複的字元字元
- 7 個改變我生活的 Git 小技巧Git
- 如何確定一個 Go 變數會被分配在哪裡?Go變數