Go 部落格

Go 程式效能分析

Russ Cox,2011 年 7 月;馬勝厚 (Shenghou Ma) 更新,2013 年 5 月
2011 年 6 月 24 日

在 2011 年的 Scala Days 會議上,Robert Hundt 發表了一篇題為 Loop Recognition in C++/Java/Go/Scala 的論文。這篇論文用 C++、Go、Java、Scala 實現了特定的迴圈查詢演算法(例如您在編譯器流分析階段可能使用的演算法),然後使用這些程式來得出關於這些語言中典型效能問題的結論。論文中提到的 Go 程式執行非常緩慢,這為演示如何使用 Go 的效能分析工具來最佳化慢速程式提供了絕佳機會。

透過使用 Go 的效能分析工具識別和糾正特定瓶頸,我們可以使 Go 的迴圈查詢程式執行速度提高一個數量級,並減少 6 倍的記憶體使用。(更新:由於最近 gcclibstdc++ 的最佳化,記憶體減少量現在是 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 記憶體、執行 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 基準測試程式,將它們合併到單個原始檔中,並刪除了除一行輸出之外的所有內容。我們將使用 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 的 pprof C++ 效能分析器的一個稍作修改的版本。最重要的命令是 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.FindLoopsmain.main 的總數應該是 100%,但每個堆疊樣本僅包含底部 100 個堆疊幀;在大約四分之一的樣本中,遞迴的 main.DFS 函式比 main.main 深 100 幀以上,因此完整的呼叫棧被截斷了。

堆疊跟蹤樣本包含比文字列表能顯示的更有趣的函式呼叫關係資料。web 命令將效能分析資料以 SVG 格式繪製成圖並使用網路瀏覽器開啟。(還有一個 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.FindLoopsmain.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 命令結合了這兩種模式:它顯示 一個原始碼列表,點選一行可以顯示相應的反彙編

既然我們已經知道時間主要花在雜湊執行時函式實現的 map 查詢上,我們最關心的是第二列。相當大一部分時間花在對 DFS 的遞迴呼叫上(第 247 行),這在遞迴遍歷中是可以預料的。排除遞迴之外,看起來時間花在了第 242、246 和 250 行對 number map 的訪問上。對於這種特定的查詢,map 不是最高效的選擇。就像在編譯器中一樣,基本塊結構被分配了唯一的序列號。與其使用 map[*BasicBlock]int,我們可以使用 []int,一個透過塊編號索引的切片。當可以使用陣列或切片時,沒有理由使用 map。

number 從 map 改為切片需要在程式中修改七行程式碼,這將使其執行時間減少近一半:

$ make havlak2
go build havlak2.go
$ ./xtime ./havlak2
# of loops: 76000 (including 1 artificial root node)
16.55u 0.11s 16.69r 1321008kB ./havlak2
$

(參見 havlak1havlak2 之間的 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 分配了當前使用中的 82.4 MB 中的大約 56.3 MB;CreateNode 佔用了另外 17.6 MB。為了降低開銷,記憶體效能分析器每分配大約半兆位元組才記錄一個塊的資訊(“每 524288 個樣本中記錄 1 個的取樣率”),因此這些是實際計數的近似值。

要找到記憶體分配,我們可以列出這些函式。

(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:     }
...

看來當前的瓶頸與上一個相同:在可以使用更簡單資料結構的地方使用了 map。FindLoops 大約分配了 29.5 MB 的 map。

順帶一提,如果我們使用 --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 個 map 佔用了 29.5 MB,看起來初始 map 分配大約需要 150 位元組。當 map 用於儲存鍵值對時,這是合理的,但當 map 被用作簡單集合的替代品時(就像這裡),這就不合理了。

除了使用 map,我們可以使用一個簡單的切片來列出元素。在使用 map 的所有情況中,除了一個例外,演算法都不可能插入重複元素。在剩下的那個例外情況中,我們可以編寫一個簡單的 append 內建函式變體:

func appendUnique(a []int, x int) []int {
    for _, y := range a {
        if x == y {
            return a
        }
    }
    return append(a, x)
}

除了編寫該函式外,將 Go 程式改為使用切片而不是 map 只需修改幾行程式碼。

$ 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

我們可以做更多的工作來清理程式並使其更快,但這都不需要我們尚未展示的效能分析技術。內迴圈中使用的工作列表可以在迭代和對 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++ 程式進行比較已不再公平,後者使用了效率低下的資料結構如 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.cchavlak6.go

基準測試的效果取決於它們所測量的程式。我們使用 go tool pprof 來研究一個效率低下的 Go 程式,然後將其效能提高了一個數量級,並將其記憶體使用量減少了 3.7 倍。隨後與等效最佳化的 C++ 程式進行比較表明,當程式設計師關注內迴圈產生的垃圾量時,Go 可以與 C++ 競爭。

用於撰寫此博文的程式原始碼、Linux x86-64 二進位制檔案和效能分析資料可在 GitHub 上的 benchgraffiti 專案中獲取。

如上所述,go test 已包含這些效能分析標誌:定義一個 基準測試函式,您就準備好了。還有一個標準的 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 阻塞效能分析將在以後的博文中解釋。敬請期待。

下一篇文章:Go 中的頭等函式 (First Class Functions)
上一篇文章:聚焦 Go 外部庫
部落格索引