這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
標籤(空格分隔): Go Memory Profiler 效能調試 效能分析
註:該文作者是 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 的現場應用的概要檔案收集非常有用,後者對程式結束的時候的概要檔案收集非常有用(否則你將看到空蕩蕩的概要檔案)。
注意:記憶體分析器很簡單,也就是說,它收集的資訊僅僅是關於記憶體配置的一些子集。機率抽樣對象與它的大小成正比,你可以使用 go test --memprofilerate
標誌改變抽樣比率,或者是在程式啟動的時候設定 runtime.MemProfileRate
變數。比率 1 將導致收集所有分配的資訊。但是它可能導致執行很慢,預設的採樣率是每 512kb 的記憶體配置 1個樣本。
你也可以顯示分配的位元組數,或者是分配的對象數量(--inuse/alloc_space
和 --inuse/alloc_objects
標誌)。分析器在分析時更傾向於大樣本對象。但是更重要的是要明白大對象影響記憶體消耗和 GC 時間,然而大量微小的分配影響執行速度(同樣是某種程度的 GC 時間),所以兩個都觀察可能是非常有用的。
對象可以是持久的或是瞬態的。如果在程式開始的時候,你有一些大的持久化對象分配,它們將最有可能被分析器採樣(因為它們足夠大)。這樣的對象會影響記憶體的消耗和 GC 時間,但是它們不影響正常的執行速度(沒有記憶體管理操作發生在它們身上)。換句話說,如果你有大量的生命週期非常短暫的對象,在概要檔案中,它們幾乎可以代表(如果你使用預設的 --inuse_space 模式),但它們很明顯的會影響執行速度。因為它們在不斷的分配和釋放。因此,再一次聲明,觀察兩種類型的對象是非常有用的。
因此,通常如果你想降低記憶體消耗,在正常的程式操作期間,你需要查看 --inuse_space
概要檔案收集。如果你想提升執行速度,查看 --alloc_objects
概要檔案收集,在重要的已耗用時間或程式結束之後。
這有一些標誌控制報告的粒度。--functions
使得 pprof
報告在函數層級(預設)。--lines
使得 pprof
報告在源碼的行層級。這是非常有用的,如果熱函數在不同的行。這裡也有 --addresses
和 --files
各自對應準確的指令地址和檔案層級。
對於記憶體概要檔案來說,這是非常有用的選項 -- 你可以在瀏覽器中查看它(提供這個功能需要你 imported net/http/pprof)。如果你開啟 http://myserver:6060/debug/pprof/heap?debug=1,你必須看到堆類似:
heap profile: 4: 266528 [123: 11284472] @ heap/10485761: 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:1412: 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]") 代表當前存活對象的數量,存活對象已經佔用的記憶體,分配的總的數量和所有分配已經佔用的記憶體。
最佳化通常特定於應用程式,但這裡有一些常見的建議。
- 對象合并成更大的對象。比如,使用 bytes.Buffer 代替 *bytes.Buffer 結構(後面你可以通過調用 bytes.Buffer.Grow 預先分配 buffer )。這將降低記憶體的分配數量(更快),同時降低記憶體回收行程的壓力(更快的記憶體回收)。
局部變數逃離了它們聲明的範圍,提升到堆分配。編譯器通常不能證明幾個變數有相同的壽命,因此它分別分配每個這樣的變數。因此你可以使用以上的建議處理局部變數,比如,把下面這個:
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 }()}
這會把兩個記憶體配置變為一個記憶體配置。儘管如此,該最佳化會影響代碼的可讀性,所以請合理使用它。
分配的一個特例就是 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}
- 如果可能的話,使用更小的資料類型,比如,使用 int8 代替 int。
- 對象不包含任何指標(注意: strings,slices, maps 和 chans 包含隱含的指標),不會被垃圾收集器掃描。比如,1GB byte 的 slice 事實上不會影響垃圾收集時間。因此如果你從已經使用的活躍的對象移除指標,肯定會影響垃圾收集時間。一些可能性:使用 indices 代替指標,把對象分割成兩部分,其中一部分不包含指標。
- 使用 freelists 重新利用瞬時對象和分配數量。標準包包含 sync.Pool 類型,在垃圾收集之間的幾次,允許重新使用相同的對象。儘管如此,要知道,任何手動記憶體管理方案, 不正確的使用 sync.Pool 可能會導致 use-after-free(釋放後使用的 bug) bugs。
你可以使用垃圾收集器跟蹤(見下文)來得到一些記憶體問題更深刻的見解。