Go 部落格

Go 程式效能分析

Russ Cox, 2011 年 7 月;Shenghou Ma, 2013 年 5 月更新
2011 年 6 月 24 日

在 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.cchavlak6.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 阻塞分析將在以後的文章中介紹。敬請關注。

下一篇文章:Go 中的一等函式
上一篇文章:聚焦外部 Go 庫
部落格索引