Go 程序的性能調試問題 - 內存篇

標籤(空格分隔): Go Memory Profiler 性能調試 性能分析golang


注:該文做者是 Dmitry Vyukov,原文地址 Debugging performance issues in Go programs數組

這個是原文中的 Memory Profiler 段落瀏覽器

內存分析器顯示了函數分配堆內存的狀況。你能夠以 CPU profile 類似的方式收集:使用 go test --memprofile,經過 http://myserver:6060:/debug/pprof/heap 使用 net/http/pprof 或是經過調用 runtime/pprof.WriteHeapProfile函數

你僅能夠顯示在概要文件收集的時間分配的內存(默認,pprof 的 --inuse_space 標誌),或是從程序啓動起的全部分配(pprof 的 --alloc_space 標誌)。前者對對於 net/http/pprof 的現場應用的概要文件收集很是有用,後者對程序結束的時候的概要文件收集很是有用(不然你將看到空蕩蕩的概要文件)。oop

注意:內存分析器很簡單,也就是說,它收集的信息僅僅是關於內存分配的一些子集。機率抽樣對象與它的大小成正比,你可使用 go test --memprofilerate 標誌改變抽樣比率,或者是在程序啓動的時候設置 runtime.MemProfileRate 變量。比率 1 將致使收集全部分配的信息。可是它可能致使執行很慢,默認的採樣率是每 512kb 的內存分配 1個樣本。性能

你也能夠顯示分配的字節數,或者是分配的對象數量(--inuse/alloc_space--inuse/alloc_objects 標誌)。分析器在分析時更傾向於大樣本對象。可是更重要的是要明白大對象影響內存消耗和 GC 時間,然而大量微小的分配影響執行速度(一樣是某種程度的 GC 時間),因此兩個都觀察多是很是有用的。優化

對象能夠是持久的或是瞬態的。若是在程序開始的時候,你有一些大的持久化對象分配,它們將最有可能被分析器採樣(由於它們足夠大)。這樣的對象會影響內存的消耗和 GC 時間,可是它們不影響正常的執行速度(沒有內存管理操做發生在它們身上)。換句話說,若是你有大量的生命週期很是短暫的對象,在概要文件中,它們幾乎能夠表明(若是你使用默認的 --inuse_space 模式),但它們很明顯的會影響執行速度。由於它們在不斷的分配和釋放。所以,再一次聲明,觀察兩種類型的對象是很是有用的。ui

所以,一般若是你想下降內存消耗,在正常的程序操做期間,你須要查看 --inuse_space 概要文件收集。若是你想提高執行速度,查看 --alloc_objects 概要文件收集,在重要的運行時間或程序結束以後。spa

這有一些標誌控制報告的粒度。--functions 使得 pprof 報告在函數級別(默認)。--lines 使得 pprof 報告在源碼的行級別。這是很是有用的,若是熱函數在不一樣的行。這裏也有 --addresses--files 各自對應準確的指令地址和文件級別。debug

對於內存概要文件來講,這是很是有用的選項 -- 你能夠在瀏覽器中查看它(提供這個功能須要你 imported net/http/pprof)。若是你打開 http://myserver:6060/debug/pprof/heap?debug=1,你必須看到堆相似:

heap profile: 4: 266528 [123: 11284472] @ heap/1048576
1: 262144 [4: 376832] @ 0x28d9f 0x2a201 0x2a28a 0x2624d 0x26188 0x94ca3 0x94a0b 0x17add6 0x17ae9f 0x1069d3 0xfe911 0xf0a3e 0xf0d22 0x21a70
#    0x2a201    cnew+0xc1    runtime/malloc.goc:718
#    0x2a28a    runtime.cnewarray+0x3a            runtime/malloc.goc:731
#    0x2624d    makeslice1+0x4d                runtime/slice.c:57
#    0x26188    runtime.makeslice+0x98            runtime/slice.c:38
#    0x94ca3    bytes.makeSlice+0x63            bytes/buffer.go:191
#    0x94a0b    bytes.(*Buffer).ReadFrom+0xcb        bytes/buffer.go:163
#    0x17add6    io/ioutil.readAll+0x156            io/ioutil/ioutil.go:32
#    0x17ae9f    io/ioutil.ReadAll+0x3f            io/ioutil/ioutil.go:41
#    0x1069d3    godoc/vfs.ReadFile+0x133            godoc/vfs/vfs.go:44
#    0xfe911    godoc.func·023+0x471            godoc/meta.go:80
#    0xf0a3e    godoc.(*Corpus).updateMetadata+0x9e        godoc/meta.go:101
#    0xf0d22    godoc.(*Corpus).refreshMetadataLoop+0x42    godoc/meta.go:141
2: 4096 [2: 4096] @ 0x28d9f 0x29059 0x1d252 0x1d450 0x106993 0xf1225 0xe1489 0xfbcad 0x21a70
#    0x1d252    newdefer+0x112                runtime/panic.c:49
#    0x1d450    runtime.deferproc+0x10            runtime/panic.c:132
#    0x106993    godoc/vfs.ReadFile+0xf3            godoc/vfs/vfs.go:43
#    0xf1225    godoc.(*Corpus).parseFile+0x75        godoc/parser.go:20
#    0xe1489    godoc.(*treeBuilder).newDirTree+0x8e9    godoc/dirtrees.go:108
#    0xfbcad    godoc.func·002+0x15d            godoc/dirtrees.go:100

在每一個入口開始的數字 ("1: 262144 [4: 376832]") 表明當前存活對象的數量,存活對象已經佔用的內存,分配的總的數量和全部分配已經佔用的內存。

優化一般特定於應用程序,但這裏有一些常見的建議。

  1. 對象合併成更大的對象。好比,使用 bytes.Buffer 代替 *bytes.Buffer 結構(後面你能夠經過調用 bytes.Buffer.Grow 預先分配 buffer )。這將下降內存的分配數量(更快),同時下降垃圾回收器的壓力(更快的垃圾回收)。
  2. 局部變量逃離了它們聲明的範圍,提高到堆分配。編譯器一般不能證實幾個變量有相同的壽命,所以它分別分配每一個這樣的變量。所以你可使用以上的建議處理局部變量,好比,把下面這個:

    for k, v := range m {
       k, v := k, v   // copy for capturing by the goroutine
       go func() {
           // use k and v
       }()
    }

    替代爲:

    for k, v := range m {
       x := struct{ k, v string }{k, v}   // copy for capturing by the goroutine
       go func() {
           // use x.k and x.v
       }()
    }

    這會把兩個內存分配變爲一個內存分配。儘管如此,該優化會影響代碼的可讀性,因此請合理使用它。

  3. 分配的一個特例就是 slice 數組預分配。若是你知道一個 slice 的標準大小,你能夠像下面這樣預分配一個支持數組:

    type X struct {
        buf      []byte
        bufArray [16]byte // Buf usually does not grow beyond 16 bytes.
    }
    
    func MakeX() *X {
        x := &X{}
        // Preinitialize buf with the backing array.
        x.buf = x.bufArray[:0]
        return x
    }
  4. 若是可能的話,使用更小的數據類型,好比,使用 int8 代替 int。
  5. 對象不包含任何指針(注意: strings,slices, maps 和 chans 包含隱含的指針),不會被垃圾收集器掃描。好比,1GB byte 的 slice 事實上不會影響垃圾收集時間。所以若是你從已經使用的活躍的對象移除指針,確定會影響垃圾收集時間。一些可能性:使用 indices 代替指針,把對象分割成兩部分,其中一部分不包含指針。
  6. 使用 freelists 從新利用瞬時對象和分配數量。標準包包含 sync.Pool 類型,在垃圾收集之間的幾回,容許從新使用相同的對象。儘管如此,要知道,任何手動內存管理方案, 不正確的使用 sync.Pool 可能會致使 use-after-free(釋放後使用的 bug) bugs。

你可使用垃圾收集器跟蹤(見下文)來獲得一些內存問題更深入的看法。

相關文章
相關標籤/搜索