Go 部落格
Go 程式效能分析
在 2011 年 Scala 大會(Scala Days 2011)上,Robert Hundt 發表了一篇題為《C++/Java/Go/Scala 中的迴圈識別》(Loop Recognition in C++/Java/Go/Scala.)的論文。該論文使用 C++、Go、Java 和 Scala 實現了一種特定的迴圈查詢演算法,類似於編譯器流分析過程中使用的演算法,然後利用這些程式得出關於這些語言典型效能關注點的結論。論文中展示的 Go 程式執行得相當慢,這為演示如何使用 Go 的效能分析工具將慢速程式變得更快提供了一個絕佳的機會。
透過使用 Go 的效能分析工具識別和糾正特定的瓶頸,我們可以使 Go 的迴圈查詢程式執行速度提高一個數量級,記憶體使用量減少 6 倍。(更新:由於最近 `gcc` 中 `libstdc++` 的最佳化,記憶體使用量現在減少了 3.7 倍。)
Hundt 的論文沒有具體說明他使用了哪些版本的 C++、Go、Java 和 Scala 工具。在這篇博文中,我們將使用 `6g` Go 編譯器的最新周快照版本,以及 Ubuntu Natty 發行版附帶的 `g++` 版本。(我們不使用 Java 或 Scala,因為我們不擅長用這兩種語言編寫高效的程式,所以比較會不公平。由於 C++ 在論文中是最快的語言,因此這裡的 C++ 比較應該足夠了。)(更新:在這篇更新的文章中,我們將使用 amd64 上 Go 編譯器的最新開發快照版本,以及 `g++` 的最新版本——4.8.0,該版本於 2013 年 3 月釋出。)
$ go version
go version devel +08d20469cc20 Tue Mar 26 08:27:18 2013 +0100 linux/amd64
$ g++ --version
g++ (GCC) 4.8.0
Copyright (C) 2013 Free Software Foundation, Inc.
...
$
程式執行在配置為 3.4GHz Core i7-2600 CPU 和 16 GB RAM 的計算機上,執行 Gentoo Linux 的 3.8.4-gentoo 核心。該機器的 CPU 頻率縮放功能已停用,透過
$ sudo bash
# for i in /sys/devices/system/cpu/cpu[0-7]
do
echo performance > $i/cpufreq/scaling_governor
done
#
我們從 Hundt 的 C++ 和 Go 基準測試程式(Hundt’s benchmark programs)中提取了程式碼,將每個程式合併到一個原始檔中,並刪除了除一行輸出以外的所有輸出。我們將使用 Linux 的 `time` 工具來計時程式,並採用一種顯示使用者時間、系統時間、實際時間以及最大記憶體使用量的格式。
$ cat xtime
#!/bin/sh
/usr/bin/time -f '%Uu %Ss %er %MkB %C' "$@"
$
$ make havlak1cc
g++ -O3 -o havlak1cc havlak1.cc
$ ./xtime ./havlak1cc
# of loops: 76002 (total 3800100)
loop-0, nest: 0, depth: 0
17.70u 0.05s 17.80r 715472kB ./havlak1cc
$
$ make havlak1
go build havlak1.go
$ ./xtime ./havlak1
# of loops: 76000 (including 1 artificial root node)
25.05u 0.11s 25.20r 1334032kB ./havlak1
$
C++ 程式執行耗時 17.80 秒,記憶體使用量為 700 MB。Go 程式執行耗時 25.20 秒,記憶體使用量為 1302 MB。(這些測量結果與論文中的結果難以調和,但本文的重點是探索如何使用 `go tool pprof`,而不是重現論文中的結果。)
為了開始最佳化 Go 程式,我們必須啟用效能分析。如果程式碼使用了 Go 的 testing 包的基準測試支援,我們可以使用 gotest 標準的 `-cpuprofile` 和 `-memprofile` 標誌。在像這樣獨立的程式中,我們必須匯入 `runtime/pprof` 並新增幾行程式碼。
var cpuprofile = flag.String("cpuprofile", "", "write cpu profile to file")
func main() {
flag.Parse()
if *cpuprofile != "" {
f, err := os.Create(*cpuprofile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
...
新程式碼定義了一個名為 `cpuprofile` 的標誌,呼叫 Go 的 flag 庫來解析命令列標誌,然後,如果命令列中設定了 `cpuprofile` 標誌,則將 CPU 效能分析重定向到該檔案。效能分析器需要在程式退出前呼叫 `StopCPUProfile` 來重新整理任何待寫入檔案的內容;我們使用 `defer` 來確保這一點在 `main` 函式返回時發生。
新增程式碼後,我們可以使用新的 `-cpuprofile` 標誌執行程式,然後執行 `go tool pprof` 來解釋分析結果。
$ make havlak1.prof
./havlak1 -cpuprofile=havlak1.prof
# of loops: 76000 (including 1 artificial root node)
$ go tool pprof havlak1 havlak1.prof
Welcome to pprof! For help, type 'help'.
(pprof)
`go tool pprof` 程式是 Google 的 C++ 效能分析器 `pprof` 的一個變體。最重要的命令是 `topN`,它顯示分析結果中排名前 `N` 的樣本。
(pprof) top10
Total: 2525 samples
298 11.8% 11.8% 345 13.7% runtime.mapaccess1_fast64
268 10.6% 22.4% 2124 84.1% main.FindLoops
251 9.9% 32.4% 451 17.9% scanblock
178 7.0% 39.4% 351 13.9% hash_insert
131 5.2% 44.6% 158 6.3% sweepspan
119 4.7% 49.3% 350 13.9% main.DFS
96 3.8% 53.1% 98 3.9% flushptrbuf
95 3.8% 56.9% 95 3.8% runtime.aeshash64
95 3.8% 60.6% 101 4.0% runtime.settype_flush
88 3.5% 64.1% 988 39.1% runtime.mallocgc
啟用 CPU 效能分析時,Go 程式大約每秒停止 100 次,並記錄一個樣本,該樣本包含當前正在執行的 goroutine 堆疊上的程式計數器。分析結果包含 2525 個樣本,因此程式運行了略多於 25 秒。在 `go tool pprof` 的輸出中,每一行代表一個出現在樣本中的函式。前兩列顯示函式執行時(相對於等待被呼叫的函式返回)的樣本數,以原始計數和總樣本百分比表示。`runtime.mapaccess1_fast64` 函式在 298 個樣本中執行,佔 11.8%。`top10` 輸出按此樣本計數排序。第三列顯示列表中的執行總計:前三行佔樣本總數的 32.4%。第四列和第五列顯示函數出現在樣本中的次數(無論是正在執行還是等待被呼叫的函式返回)。`main.FindLoops` 函式在 10.6% 的樣本中執行,但在呼叫堆疊上(它或它呼叫的函式正在執行)的樣本佔 84.1%。
要按第四列和第五列排序,請使用 `-cum`(代表累積)標誌。
(pprof) top5 -cum
Total: 2525 samples
0 0.0% 0.0% 2144 84.9% gosched0
0 0.0% 0.0% 2144 84.9% main.main
0 0.0% 0.0% 2144 84.9% runtime.main
0 0.0% 0.0% 2124 84.1% main.FindHavlakLoops
268 10.6% 10.6% 2124 84.1% main.FindLoops
(pprof) top5 -cum
實際上,`main.FindLoops` 和 `main.main` 的總計應該為 100%,但每個堆疊樣本僅包含底部 100 個堆疊幀;在約四分之一的樣本中,遞迴函式 `main.DFS` 的深度超過 `main.main` 100 幀以上,因此完整的跟蹤被截斷了。
堆疊跟蹤樣本包含比文字列表更豐富的函式呼叫關係資料。`web` 命令以 SVG 格式寫入效能分析資料圖,並在 Web 瀏覽器中開啟它。(還有一個 `gv` 命令,它寫入 PostScript 並在 Ghostview 中開啟它。對於這兩個命令,都需要安裝 graphviz。)
(pprof) web
下面是 完整圖表 的一小部分。

圖中的每個框代表一個單獨的函式,框的大小根據函式執行的樣本數量確定。從框 X 到框 Y 的邊表示 X 呼叫 Y;邊上的數字表示該呼叫在樣本中出現的次數。如果一個呼叫在單個樣本中出現多次,例如在遞迴函式呼叫期間,則每次出現都會計入邊的權重。這解釋了 `main.DFS` 到其自身的自迴圈邊上的 21342。
僅僅從圖中,我們就可以看到程式花費了大量時間在雜湊操作上,這對應於 Go 的 `map` 值的用法。我們可以告訴 `web` 命令只使用包含特定函式的樣本,例如 `runtime.mapaccess1_fast64`,這可以清除圖中的一些噪聲。
(pprof) web mapaccess1

如果我們仔細觀察,我們可以看到對 `runtime.mapaccess1_fast64` 的呼叫是由 `main.FindLoops` 和 `main.DFS` 發出的。
現在我們對大局有了一個大致的瞭解,是時候深入研究一個特定的函數了。讓我們先看看 `main.DFS`,因為它是一個較短的函式。
(pprof) list DFS
Total: 2525 samples
ROUTINE ====================== main.DFS in /home/rsc/g/benchgraffiti/havlak/havlak1.go
119 697 Total samples (flat / cumulative)
3 3 240: func DFS(currentNode *BasicBlock, nodes []*UnionFindNode, number map[*BasicBlock]int, last []int, current int) int {
1 1 241: nodes[current].Init(currentNode, current)
1 37 242: number[currentNode] = current
. . 243:
1 1 244: lastid := current
89 89 245: for _, target := range currentNode.OutEdges {
9 152 246: if number[target] == unvisited {
7 354 247: lastid = DFS(target, nodes, number, last, lastid+1)
. . 248: }
. . 249: }
7 59 250: last[number[currentNode]] = lastid
1 1 251: return lastid
(pprof)
列表顯示了 `DFS` 函式的原始碼(實際上是匹配正則表示式 `DFS` 的所有函式的原始碼)。前三列是執行該行時採取的樣本數,執行該行或在呼叫該行的程式碼時採取的樣本數,以及檔案中的行號。相關的 `disasm` 命令顯示函式的反彙編而不是原始碼列表;當樣本數足夠時,這可以幫助您檢視哪些指令成本高昂。`weblist` 命令混合了這兩種模式:它顯示 原始碼列表,點選某一行會顯示其反彙編。
由於我們已經知道時間花在了由雜湊執行時函式實現的對映查詢上,因此我們最關心第二列。大量的執行時間花在了對 `DFS` 的遞迴呼叫(第 247 行)上,這符合遞迴遍歷的預期。排除遞迴,看起來時間花在了第 242、246 和 250 行的 `number` 對映的訪問上。對於這種特定的查詢,對映不是最有效的選擇。正如在編譯器中一樣,基本塊結構被分配了唯一的序號。我們可以使用 `[]int`(一個由塊號索引的切片),而不是使用 `map[*BasicBlock]int`。當陣列或切片可以勝任時,沒有理由使用對映。
將 `number` 從對映更改為切片需要修改程式中的七行程式碼,並將執行時間縮短近一半。
$ make havlak2
go build havlak2.go
$ ./xtime ./havlak2
# of loops: 76000 (including 1 artificial root node)
16.55u 0.11s 16.69r 1321008kB ./havlak2
$
(請參閱 `havlak1` 和 `havlak2` 之間的 diff)
我們可以再次執行效能分析器來確認 `main.DFS` 不再是執行時的重要組成部分。
$ make havlak2.prof
./havlak2 -cpuprofile=havlak2.prof
# of loops: 76000 (including 1 artificial root node)
$ go tool pprof havlak2 havlak2.prof
Welcome to pprof! For help, type 'help'.
(pprof)
(pprof) top5
Total: 1652 samples
197 11.9% 11.9% 382 23.1% scanblock
189 11.4% 23.4% 1549 93.8% main.FindLoops
130 7.9% 31.2% 152 9.2% sweepspan
104 6.3% 37.5% 896 54.2% runtime.mallocgc
98 5.9% 43.5% 100 6.1% flushptrbuf
(pprof)
`main.DFS` 條目不再出現在效能分析結果中,其餘程式的執行時也已下降。現在程式大部分時間都在分配記憶體和垃圾回收(`runtime.mallocgc`,它同時進行分配和定期垃圾回收,佔用了 54.2% 的時間)。要找出垃圾回收器執行如此頻繁的原因,我們需要找出是什麼在分配記憶體。一種方法是將記憶體分析新增到程式中。我們將安排,如果提供了 `-memprofile` 標誌,程式將在迴圈查詢執行一次迭代後停止,寫入記憶體分析結果並退出。
var memprofile = flag.String("memprofile", "", "write memory profile to this file")
...
FindHavlakLoops(cfgraph, lsgraph)
if *memprofile != "" {
f, err := os.Create(*memprofile)
if err != nil {
log.Fatal(err)
}
pprof.WriteHeapProfile(f)
f.Close()
return
}
我們使用 `-memprofile` 標誌呼叫程式來寫入分析結果。
$ make havlak3.mprof
go build havlak3.go
./havlak3 -memprofile=havlak3.mprof
$
(請參閱 從 havlak2 的 diff)
我們使用 `go tool pprof` 的方式完全相同。現在我們正在檢查的樣本是記憶體分配,而不是時鐘滴答。
$ go tool pprof havlak3 havlak3.mprof
Adjusting heap profiles for 1-in-524288 sampling rate
Welcome to pprof! For help, type 'help'.
(pprof) top5
Total: 82.4 MB
56.3 68.4% 68.4% 56.3 68.4% main.FindLoops
17.6 21.3% 89.7% 17.6 21.3% main.(*CFG).CreateNode
8.0 9.7% 99.4% 25.6 31.0% main.NewBasicBlockEdge
0.5 0.6% 100.0% 0.5 0.6% itab
0.0 0.0% 100.0% 0.5 0.6% fmt.init
(pprof)
`go tool pprof` 命令報告 `FindLoops` 分配了約 56.3 MB 的可用記憶體(總共 82.4 MB),`CreateNode` 佔了另外 17.6 MB。為了減少開銷,記憶體分析器僅記錄每半兆位元組分配的約一個塊的資訊(“1-in-524288 取樣率”),因此這些是實際計數的近似值。
為了查詢記憶體分配,我們可以列出那些函式。
(pprof) list FindLoops
Total: 82.4 MB
ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go
56.3 56.3 Total MB (flat / cumulative)
...
1.9 1.9 268: nonBackPreds := make([]map[int]bool, size)
5.8 5.8 269: backPreds := make([][]int, size)
. . 270:
1.9 1.9 271: number := make([]int, size)
1.9 1.9 272: header := make([]int, size, size)
1.9 1.9 273: types := make([]int, size, size)
1.9 1.9 274: last := make([]int, size, size)
1.9 1.9 275: nodes := make([]*UnionFindNode, size, size)
. . 276:
. . 277: for i := 0; i < size; i++ {
9.5 9.5 278: nodes[i] = new(UnionFindNode)
. . 279: }
...
. . 286: for i, bb := range cfgraph.Blocks {
. . 287: number[bb.Name] = unvisited
29.5 29.5 288: nonBackPreds[i] = make(map[int]bool)
. . 289: }
...
看來當前的瓶頸與上一個相同:在可以使用更簡單資料結構的場合使用了對映。`FindLoops` 分配了大約 29.5 MB 的對映。
順帶一提,如果我們使用 `--inuse_objects` 標誌執行 `go tool pprof`,它將報告分配計數而不是大小。
$ go tool pprof --inuse_objects havlak3 havlak3.mprof
Adjusting heap profiles for 1-in-524288 sampling rate
Welcome to pprof! For help, type 'help'.
(pprof) list FindLoops
Total: 1763108 objects
ROUTINE ====================== main.FindLoops in /home/rsc/g/benchgraffiti/havlak/havlak3.go
720903 720903 Total objects (flat / cumulative)
...
. . 277: for i := 0; i < size; i++ {
311296 311296 278: nodes[i] = new(UnionFindNode)
. . 279: }
. . 280:
. . 281: // Step a:
. . 282: // - initialize all nodes as unvisited.
. . 283: // - depth-first traversal and numbering.
. . 284: // - unreached BB's are marked as dead.
. . 285: //
. . 286: for i, bb := range cfgraph.Blocks {
. . 287: number[bb.Name] = unvisited
409600 409600 288: nonBackPreds[i] = make(map[int]bool)
. . 289: }
...
(pprof)
由於約 200,000 個對映佔用了 29.5 MB,看來初始對映分配大約需要 150 位元組。當對映用於儲存鍵值對時,這是合理的,但當對映用作簡單集合的佔位符時,就不那麼合理了,這裡就是這種情況。
與其使用對映,不如使用一個簡單的切片來列出元素。在幾乎所有使用對映的情況下,演算法都不可能插入重複元素。在最後一種情況中,我們可以編寫一個簡單的 `append` 內建函式的變體。
func appendUnique(a []int, x int) []int {
for _, y := range a {
if x == y {
return a
}
}
return append(a, x)
}
除了編寫該函式之外,將 Go 程式中的對映更改為切片只需要更改幾行程式碼。
$ make havlak4
go build havlak4.go
$ ./xtime ./havlak4
# of loops: 76000 (including 1 artificial root node)
11.84u 0.08s 11.94r 810416kB ./havlak4
$
(請參閱 從 havlak3 的 diff)
我們現在的速度比開始時快了 2.11 倍。讓我們再次檢視 CPU 分析結果。
$ make havlak4.prof
./havlak4 -cpuprofile=havlak4.prof
# of loops: 76000 (including 1 artificial root node)
$ go tool pprof havlak4 havlak4.prof
Welcome to pprof! For help, type 'help'.
(pprof) top10
Total: 1173 samples
205 17.5% 17.5% 1083 92.3% main.FindLoops
138 11.8% 29.2% 215 18.3% scanblock
88 7.5% 36.7% 96 8.2% sweepspan
76 6.5% 43.2% 597 50.9% runtime.mallocgc
75 6.4% 49.6% 78 6.6% runtime.settype_flush
74 6.3% 55.9% 75 6.4% flushptrbuf
64 5.5% 61.4% 64 5.5% runtime.memmove
63 5.4% 66.8% 524 44.7% runtime.growslice
51 4.3% 71.1% 51 4.3% main.DFS
50 4.3% 75.4% 146 12.4% runtime.MCache_Alloc
(pprof)
現在,記憶體分配和由此產生的垃圾回收(`runtime.mallocgc`)佔用了我們執行時間的 50.9%。另一種檢視系統垃圾回收原因的方法是檢視導致回收的分配,即花費大部分時間在 `mallocgc` 上的那些分配。
(pprof) web mallocgc

很難弄清楚圖中的情況,因為有許多樣本數較小的節點遮擋了較大的節點。我們可以告訴 `go tool pprof` 忽略那些不佔總樣本 10% 以上的節點。
$ go tool pprof --nodefraction=0.1 havlak4 havlak4.prof
Welcome to pprof! For help, type 'help'.
(pprof) web mallocgc

現在我們可以輕鬆地跟蹤粗箭頭,發現 `FindLoops` 觸發了大部分垃圾回收。如果我們列出 `FindLoops`,我們會發現它大部分時間都花在了開頭。
(pprof) list FindLoops
...
. . 270: func FindLoops(cfgraph *CFG, lsgraph *LSG) {
. . 271: if cfgraph.Start == nil {
. . 272: return
. . 273: }
. . 274:
. . 275: size := cfgraph.NumNodes()
. . 276:
. 145 277: nonBackPreds := make([][]int, size)
. 9 278: backPreds := make([][]int, size)
. . 279:
. 1 280: number := make([]int, size)
. 17 281: header := make([]int, size, size)
. . 282: types := make([]int, size, size)
. . 283: last := make([]int, size, size)
. . 284: nodes := make([]*UnionFindNode, size, size)
. . 285:
. . 286: for i := 0; i < size; i++ {
2 79 287: nodes[i] = new(UnionFindNode)
. . 288: }
...
(pprof)
每次呼叫 `FindLoops` 時,它都會分配一些相當大的簿記結構。由於基準測試呼叫 `FindLoops` 50 次,這些累加起來就產生了大量的垃圾,因此也為垃圾回收器帶來了大量工作。
擁有一個垃圾回收語言並不意味著你可以忽略記憶體分配問題。在這種情況下,一個簡單的解決方案是引入一個快取,以便每次呼叫 `FindLoops` 時都可以重用前一次呼叫的儲存(如果可能)。(實際上,在 Hundt 的論文中,他解釋說 Java 程式只需要進行此項更改即可獲得合理的效能,但他在其他垃圾回收實現中並未進行相同的更改。)
我們將新增一個全域性 `cache` 結構。
var cache struct {
size int
nonBackPreds [][]int
backPreds [][]int
number []int
header []int
types []int
last []int
nodes []*UnionFindNode
}
然後讓 `FindLoops` 在分配時諮詢它作為替代。
if cache.size < size {
cache.size = size
cache.nonBackPreds = make([][]int, size)
cache.backPreds = make([][]int, size)
cache.number = make([]int, size)
cache.header = make([]int, size)
cache.types = make([]int, size)
cache.last = make([]int, size)
cache.nodes = make([]*UnionFindNode, size)
for i := range cache.nodes {
cache.nodes[i] = new(UnionFindNode)
}
}
nonBackPreds := cache.nonBackPreds[:size]
for i := range nonBackPreds {
nonBackPreds[i] = nonBackPreds[i][:0]
}
backPreds := cache.backPreds[:size]
for i := range nonBackPreds {
backPreds[i] = backPreds[i][:0]
}
number := cache.number[:size]
header := cache.header[:size]
types := cache.types[:size]
last := cache.last[:size]
nodes := cache.nodes[:size]
當然,這樣的全域性變數是不好的工程實踐:它意味著併發呼叫 `FindLoops` 現在是不安全的。目前,我們只進行最少的更改,以瞭解程式效能的關鍵因素;此更改很簡單,並且模仿了 Java 實現中的程式碼。Go 程式的最終版本將使用一個單獨的 `LoopFinder` 例項來跟蹤此記憶體,恢復了併發使用的可能性。
$ make havlak5
go build havlak5.go
$ ./xtime ./havlak5
# of loops: 76000 (including 1 artificial root node)
8.03u 0.06s 8.11r 770352kB ./havlak5
$
(請參閱 從 havlak4 的 diff)
我們還可以做更多工作來清理程式並使其更快,但所有這些都不需要我們尚未展示的效能分析技術。內部迴圈中使用的 work list 可以在迭代之間和呼叫 `FindLoops` 之間重用,並且可以與該傳遞過程中生成的單獨的“節點池”結合使用。同樣,迴圈圖儲存可以在每次迭代時重用,而不是重新分配。除了這些效能改進之外,最終版本使用了慣用的 Go 風格編寫,並使用了資料結構和方法。風格上的改變對執行時間影響很小:演算法和約束保持不變。
最終版本執行耗時 2.29 秒,記憶體使用量為 351 MB。
$ make havlak6
go build havlak6.go
$ ./xtime ./havlak6
# of loops: 76000 (including 1 artificial root node)
2.26u 0.02s 2.29r 360224kB ./havlak6
$
這比我們開始時使用的程式快了 11 倍。即使我們停用生成的迴圈圖的重用,只快取迴圈查詢簿記資訊,程式仍然比原始程式快 6.7 倍,記憶體使用量減少 1.5 倍。
$ ./xtime ./havlak6 -reuseloopgraph=false
# of loops: 76000 (including 1 artificial root node)
3.69u 0.06s 3.76r 797120kB ./havlak6 -reuseloopgraph=false
$
當然,現在將這個 Go 程式與原始 C++ 程式進行比較已經不公平了,因為原始 C++ 程式使用了像 `set` 這樣效率低下的資料結構,而 `vector` 會更合適。作為一項健全性檢查,我們將最終的 Go 程式翻譯成了等效的 C++ 程式碼。其執行時間與 Go 程式相似。
$ make havlak6cc
g++ -O3 -o havlak6cc havlak6.cc
$ ./xtime ./havlak6cc
# of loops: 76000 (including 1 artificial root node)
1.99u 0.19s 2.19r 387936kB ./havlak6cc
Go 程式執行速度幾乎與 C++ 程式一樣快。由於 C++ 程式使用自動刪除和分配而不是顯式快取,因此 C++ 程式稍短且易於編寫,但並非戲劇性地如此。
$ wc havlak6.cc; wc havlak6.go
401 1220 9040 havlak6.cc
461 1441 9467 havlak6.go
$
(請參閱 havlak6.cc 和 havlak6.go)
基準測試僅與其衡量的程式一樣好。我們使用 `go tool pprof` 研究了一個效率低下的 Go 程式,然後透過一個數量級提高了其效能,並將記憶體使用量減少了 3.7 倍。隨後與等效最佳化的 C++ 程式進行比較表明,當程式設計師在內部迴圈中生成的垃圾量方面做到仔細時,Go 可以與 C++ 競爭。
用於編寫本文的程式原始檔、Linux x86-64 二進位制檔案和效能分析結果可在 GitHub 上的 benchgraffiti 專案中找到。
如上所述,`go test` 已經包含了這些效能分析標誌:定義一個 benchmark 函式,您就準備好了。還有一個標準的 HTTP 介面可用於訪問效能分析資料。在 HTTP 伺服器中,新增
import _ "net/http/pprof"
將在 ` /debug/pprof/` 下安裝一些 URL 的處理程式。然後,您可以使用單個引數執行 `go tool pprof`——即指向您伺服器效能分析資料的 URL,它將下載並檢查即時分析結果。
go tool pprof https://:6060/debug/pprof/profile # 30-second CPU profile
go tool pprof https://:6060/debug/pprof/heap # heap profile
go tool pprof https://:6060/debug/pprof/block # goroutine blocking profile
goroutine 阻塞分析將在以後的文章中介紹。敬請關注。