Go 語言機制之記憶體剖析(Language Mechanics On Memory Profiling)

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。## 前序(Prelude)本系列文章總共四篇,主要協助大家理解 Go 語言中一些文法結構和其背後的設計原則,包括指標、棧、堆、逃逸分析和值/指標傳遞。這是第三篇,主要介紹堆和逃逸分析。(譯者註:這一篇可看成第二篇的進階版)以下是本系列文章的索引:1. [Go 語言機制之棧與指標](https://studygolang.com/articles/12443)2. [Go 語言機制之逃逸分析](https://studygolang.com/articles/12444)3. [Go 語言機制之記憶體剖析](https://studygolang.com/articles/12445)4. [Go 語言機制之資料和文法的設計哲學](https://studygolang.com/articles/12487)觀看這段範例程式碼的視頻示範:[GopherCon Singapore (2017) - Escape Analysis](https://engineers.sg/video/go-concurrency-live-gophercon-sg-2017--1746)## 介紹(Introduction)在前面的博文中,通過一個共用在 goroutine 的棧上的值的例子講解了逃逸分析的基礎。還有其他沒有介紹的造成值逃逸的情境。為了協助大家理解,我將調試一個分配記憶體的程式,並使用非常有趣的方法。## 程式(The Program)我想瞭解 `io` 包,所以我建立了一個簡單的項目。給定一個字元序列,寫一個函數,可以找到字串 `elvis` 並用大寫開頭的 `Elvis` 替換它。我們正在討論國王(Elvis 即貓王,搖滾明星),他的名字總是大寫的。這是一個解決方案的連結:[https://play.golang.org/p/n_SzF4Cer4](https://play.golang.org/p/n_SzF4Cer4)這是一個壓力測試的連結:[https://play.golang.org/p/TnXrxJVfLV](https://play.golang.org/p/TnXrxJVfLV)代碼清單裡面有兩個不同的函數可以解決這個問題。這篇博文將會關注(其中的)`algOne` 函數,因為它使用到了 `io` 庫。你可以自己用下 `algTwo`,體驗一下記憶體,CPU 限定的差異。### 清單 1```Input:abcelvisaElvisabcelviseelvisaelvisaabeeeelvise l v i saa bb e l v i saa elviselvielviselvielvielviselvi1elvielviselvisOutput:abcElvisaElvisabcElviseElvisaElvisaabeeeElvise l v i saa bb e l v i saa elviselviElviselvielviElviselvi1elviElvisElvis```這是完整的 `algOne` 函數。### 清單 2 ```gofunc algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) { // Use a bytes Buffer to provide a stream to process. input := bytes.NewBuffer(data) // The number of bytes we are looking for. size := len(find) // Declare the buffers we need to process the stream. buf := make([]byte, size) end := size - 1 // Read in an initial number of bytes we need to get started. if n, err := io.ReadFull(input, buf[:end]); err != nil { output.Write(buf[:n]) return } for { // Read in one byte from the input stream. if _, err := io.ReadFull(input, buf[end:]); err != nil { // Flush the reset of the bytes we have. output.Write(buf[:end]) return } // If we have a match, replace the bytes. if bytes.Compare(buf, find) == 0 { output.Write(repl) // Read a new initial number of bytes. if n, err := io.ReadFull(input, buf[:end]); err != nil { output.Write(buf[:n]) return } continue } // Write the front byte since it has been compared. output.WriteByte(buf[0]) // Slice that front byte out. copy(buf, buf[1:]) }}```我想知道的是這個函數的效能表現得怎麼樣,以及它在堆上分配帶來什麼樣的壓力。為了這個目的,我們將進行壓力測試。## 壓力測試(Benchmarking)這個是我寫的壓力測試函數,它在內部調用 `algOne` 函數去處理資料流。### 清單 3```gofunc BenchmarkAlgorithmOne(b *testing.B) { var output bytes.Buffer in := assembleInputStream() find := []byte("elvis") repl := []byte("Elvis") b.ResetTimer() for i := 0; i < b.N; i++ { output.Reset() algOne(in, find, repl, &output) }}```有這個壓力測試函數,我們就可以運行 `go test` 並使用 `-bench`,`-benchtime` 和 `-benchmem` 選項。### 清單 4```$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmemBenchmarkAlgorithmOne-8 2000000 2522 ns/op 117 B/op 2 allocs/op```運行完壓力測試後,我們可以看到 `algOne` 函數分配了兩次值,每次分配了 117 個位元組。這真的很棒,但我們還需要知道哪行代碼造成了分配。為了這個目的,我們需要產生壓力測試的分析資料。## 效能分析(Profiling)為了產生分析資料,我們將再次運行壓力測試,但這次為了產生記憶體檢測資料,我們開啟 `-memprofile` 開關。### 清單 5```$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.outBenchmarkAlgorithmOne-8 2000000 2570 ns/op 117 B/op 2 allocs/op```一旦壓力測試完成,測試載入器就會產生兩個新的檔案。### 清單 6```~/code/go/src/.../memcpu$ ls -ltotal 9248-rw-r--r-- 1 bill staff 209 May 22 18:11 mem.out (NEW)-rwxr-xr-x 1 bill staff 2847600 May 22 18:10 memcpu.test (NEW)-rw-r--r-- 1 bill staff 4761 May 22 18:01 stream.go-rw-r--r-- 1 bill staff 880 May 22 14:49 stream_test.go```源碼在 `memcpu` 目錄中,`algOne` 函數在 `stream.go` 檔案中,壓力測試函數在 `stream_test.go` 檔案中。新產生的檔案為 `mem.out` 和 `memcpu.test`。`mem.out` 包含分析資料和 `memcpu.test` 檔案,以及包含我們查看分析資料時需要訪問符號的二進位檔案。有了分析資料和二進位測試檔案,我們就可以運行 `pprof` 工具學習資料分析。### 清單 7```$ go tool pprof -alloc_space memcpu.test mem.outEntering interactive mode (type "help" for commands)(pprof) _```當分析記憶體資料時,為了輕而易舉地得到我們要的資訊,你會想用 `-alloc_space` 選項替代預設的 `-inuse_space` 選項。這將會向你展示每一次分配發生在哪裡,不管你分析資料時它是不是還在記憶體中。在 `(pprof)` 提示下,我們使用 `list` 命令檢查 `algOne` 函數。這個命令可以使用Regex作為參數找到你要的函數。### 清單 8```(pprof) list algOneTotal: 335.03MBROUTINE ======================== .../memcpu.algOne in code/go/src/.../memcpu/stream.go 335.03MB 335.03MB (flat, cum) 100% of Total . . 78: . . 79:// algOne is one way to solve the problem. . . 80:func algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) { . . 81: . . 82: // Use a bytes Buffer to provide a stream to process. 318.53MB 318.53MB 83: input := bytes.NewBuffer(data) . . 84: . . 85: // The number of bytes we are looking for. . . 86: size := len(find) . . 87: . . 88: // Declare the buffers we need to process the stream. 16.50MB 16.50MB 89: buf := make([]byte, size) . . 90: end := size - 1 . . 91: . . 92: // Read in an initial number of bytes we need to get started. . . 93: if n, err := io.ReadFull(input, buf[:end]); err != nil || n < end { . . 94: output.Write(buf[:n])(pprof) _```基於這次的資料分析,我們現在知道了 `input`,`buf` 數組在堆中分配。因為 `input` 是指標變數,分析資料表明 `input` 指標變數指定的 `bytes.Buffer` 值分配了。我們先關注 `input` 記憶體配置以及弄清楚為啥會被分配。我們可以假定它被分配是因為調用 `bytes.NewBuffer` 函數時在棧上共用了 `bytes.Buffer` 值。然而,存在於 `flat` 列(pprof 輸出的第一列)的值告訴我們值被分配是因為 `algOne` 函數共用造成了它的逃逸。我知道 `flat` 列代表在函數中的分配是因為 `list` 命令顯示 `Benchmark` 函數中調用了 `aglOne`。### 清單 9```(pprof) list BenchmarkTotal: 335.03MBROUTINE ======================== .../memcpu.BenchmarkAlgorithmOne in code/go/src/.../memcpu/stream_test.go 0 335.03MB (flat, cum) 100% of Total . . 18: find := []byte("elvis") . . 19: repl := []byte("Elvis") . . 20: . . 21: b.ResetTimer() . . 22: . 335.03MB 23: for i := 0; i < b.N; i++ { . . 24: output.Reset() . . 25: algOne(in, find, repl, &output) . . 26: } . . 27:} . . 28:(pprof) _```因為在 `cum` 列(第二列)只有一個值,這告訴我 `Benchmark` 沒有直接分配。所有的記憶體配置都發生在函數調用的迴圈裡。你可以看到這兩個 `list` 調用的分配次數是匹配的。我們還是不知道為什麼 `bytes.Buffer` 值被分配。這時在 `go build` 的時候開啟 `-gcflags "-m -m"` 就派上用場了。分析資料只能告訴你哪些值逃逸,但編譯命令可以告訴你為啥。## 編譯器報告(Compiler Reporting)讓我們看一下編譯器關於代碼中逃逸分析的判決。### 清單 10```bashgo build -gcflags "-m -m"```這個命令產生了一大堆的輸出。我們只需要搜尋輸出中包含 `stream.go:83`,因為 `stream.go` 是包含這段代碼的檔案名稱並且第 83 行包含 `bytes.Buffer` 的值。搜尋後我們找到 6 行。### 清單 11```./stream.go:83: inlining call to bytes.NewBuffer func([]byte) *bytes.Buffer { return &bytes.Buffer literal }./stream.go:83: &bytes.Buffer literal escapes to heap./stream.go:83: from ~r0 (assign-pair) at ./stream.go:83./stream.go:83: from input (assigned) at ./stream.go:83./stream.go:83: from input (interface-converted) at ./stream.go:93./stream.go:83: from input (passed to call[argument escapes]) at ./stream.go:93```我們搜尋 `stream.go:83` 找到的第一行很有趣。### 清單 12```./stream.go:83: inlining call to bytes.NewBuffer func([]byte) *bytes.Buffer { return &bytes.Buffer literal }```可以肯定 `bytes.Buffer` 值沒有逃逸,因為它傳遞給了調用棧。這是因為沒有調用 `bytes.NewBuffer`,函數內聯處理了。所以這是我寫的程式碼片段:## 清單 13 ```83 input := bytes.NewBuffer(data)```因為編譯器選擇內聯 `bytes.NewBuffer` 函數調用,我寫的代碼被轉成:### 清單 14```input := &bytes.Buffer{buf: data}```這意味著 `algOne` 函數直接構造 `bytes.Buffer` 值。那麼,現在的問題是什麼造成了值從 `algOne` 棧幀中逃逸?答案在我們搜尋結果中的另外 5 行。### 清單 15```./stream.go:83: &bytes.Buffer literal escapes to heap./stream.go:83: from ~r0 (assign-pair) at ./stream.go:83./stream.go:83: from input (assigned) at ./stream.go:83./stream.go:83: from input (interface-converted) at ./stream.go:93./stream.go:83: from input (passed to call[argument escapes]) at ./stream.go:93```這幾行告訴我們代碼中的第 93 行造成了逃逸。`input` 變數被賦值給一個介面變數。## 介面(Interfaces)我完全不記得在代碼中將值賦給了介面變數。然而,如果你看到 93 行,就可以非常清楚地看到發生了什麼。### 清單 16``` 93 if n, err := io.ReadFull(input, buf[:end]); err != nil { 94 output.Write(buf[:n]) 95 return 96 }````io.ReadFull` 調用造成了介面賦值。如果你看了 `io.ReadFull` 函數的定義,你可以看到一個介面類型是如何接收 `input` 值。### 清單 17```gotype Reader interface { Read(p []byte) (n int, err error)}func ReadFull(r Reader, buf []byte) (n int, err error) { return ReadAtLeast(r, buf, len(buf))}```傳遞 `bytes.Buffer` 地址到調用棧,在 `Reader` 介面變數中儲存會造成一次逃逸。現在我們知道使用介面變數是需要開銷的:分配和重新導向。所以,如果沒有很明顯的使用介面的原因,你可能不想使用介面。下面是我選擇在My Code中是否使用介面的原則。使用介面的情況:- 使用者 API 需要提供實現細節的時候。- API 的內部需要維護多種實現。- 可以改變的 API 部分已經被識別並需要解耦。不使用介面的情況:- 為了使用介面而使用介面。- 推廣演算法。- 當使用者可以定義自己的介面時。現在我們可以問自己,這個演算法真的需要 `io.ReadFull 函數嗎?答案是否定的,因為 `bytes.Buffer` 類型有一個方法可以供我們使用。使用方法而不是調用一個函數可以防止重新分配記憶體。讓我們修改代碼,刪除 `io` 包,並直接使用 `Read` 函數而不是 `input` 變數。修改後的代碼刪除了 `io` 包的調用,為了保留相同的行號,我使用空標誌符替代 `io` 包的引用。這會允許(沒有使用的)庫匯入的行待在列表中。### 清單 18```goimport ( "bytes" "fmt" _ "io")func algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) { // Use a bytes Buffer to provide a stream to process. input := bytes.NewBuffer(data) // The number of bytes we are looking for. size := len(find) // Declare the buffers we need to process the stream. buf := make([]byte, size) end := size - 1 // Read in an initial number of bytes we need to get started. if n, err := input.Read(buf[:end]); err != nil || n < end { output.Write(buf[:n]) return } for { // Read in one byte from the input stream. if _, err := input.Read(buf[end:]); err != nil { // Flush the reset of the bytes we have. output.Write(buf[:end]) return } // If we have a match, replace the bytes. if bytes.Compare(buf, find) == 0 { output.Write(repl) // Read a new initial number of bytes. if n, err := input.Read(buf[:end]); err != nil || n < end { output.Write(buf[:n]) return } continue } // Write the front byte since it has been compared. output.WriteByte(buf[0]) // Slice that front byte out. copy(buf, buf[1:]) }}```修改後我們執行壓力測試,可以看到 `bytes.Buffer` 的分配消失了。### 清單 19```$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.outBenchmarkAlgorithmOne-8 2000000 1814 ns/op 5 B/op 1 allocs/op```我們可以看到大約 29% 的效能提升。代碼從 `2570 ns/op` 降到 `1814 ns/op`。解決了這個問題,我們現在可以關注 `buf` 切片數組。如果再次使用測試代碼產生分析資料,我們應該能夠識別到造成剩下的分配的原因。### 清單 20```$ go tool pprof -alloc_space memcpu.test mem.outEntering interactive mode (type "help" for commands)(pprof) list algOneTotal: 7.50MBROUTINE ======================== .../memcpu.BenchmarkAlgorithmOne in code/go/src/.../memcpu/stream_test.go 11MB 11MB (flat, cum) 100% of Total . . 84: . . 85: // The number of bytes we are looking for. . . 86: size := len(find) . . 87: . . 88: // Declare the buffers we need to process the stream. 11MB 11MB 89: buf := make([]byte, size) . . 90: end := size - 1 . . 91: . . 92: // Read in an initial number of bytes we need to get started. . . 93: if n, err := input.Read(buf[:end]); err != nil || n < end { . . 94: output.Write(buf[:n])```只剩下 89 行所示,對數組切片的分配。## 棧幀想知道造成 `buf` 數組切片的分配的原因?讓我們再次運行 `go build`,並使用 `-gcflags "-m -m"` 選項並搜尋 `stream.go:89`。### 清單 21```$ go build -gcflags "-m -m"./stream.go:89: make([]byte, size) escapes to heap./stream.go:89: from make([]byte, size) (too large for stack) at ./stream.go:89```報告顯示,對於棧來說,數組太大了。這個資訊誤導了我們。並不是說底層的數組太大,而是編譯器在編譯時間並不知道數組的大小。值只有在編譯器編譯時間知道其大小才會將它分配到棧中。這是因為每個函數的棧幀大小是在編譯時間計算的。如果編譯器不知道其大小,就只會在堆中分配。為了驗證(我們的想法),我們將值寫入程式碼為 5,然後再次運行壓力測試。### 清單 22```89 buf := make([]byte, 5)```這一次我們運行壓力測試,分配消失了。### 清單 23```$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmemBenchmarkAlgorithmOne-8 3000000 1720 ns/op 0 B/op 0 allocs/op```如果你再看一下編譯器報告,你會發現沒有需要逃逸處理的。### 清單 24```$ go build -gcflags "-m -m"./stream.go:83: algOne &bytes.Buffer literal does not escape./stream.go:89: algOne make([]byte, 5) does not escape```很明顯我們無法確定切片的大小,所以我們在演算法中需要一次分配。## 分配和效能(Allocation and Performance)比較一下我們在重構過程中,每次提升的效能。### 清單 25```Before any optimizationBenchmarkAlgorithmOne-8 2000000 2570 ns/op 117 B/op 2 allocs/opRemoving the bytes.Buffer allocationBenchmarkAlgorithmOne-8 2000000 1814 ns/op 5 B/op 1 allocs/opRemoving the backing array allocationBenchmarkAlgorithmOne-8 3000000 1720 ns/op 0 B/op 0 allocs/op```刪除掉 bytes.Buffer 裡面的(重新)記憶體配置,我們獲得了大約 29% 的效能提升,刪除掉所有的分配,我們能獲得大約 33% 的效能提升。記憶體配置是應用程式效能影響因素之一。## 結論(Conclusion)Go 擁有一些神奇的工具使你能瞭解編譯器作出的跟逃逸分析相關的一些決定。基於這些資訊,你可以通過重構代碼使得值存在於棧中而不需要在(被重新分配到)堆中。你不是想去掉所有軟體中所有的記憶體(再)分配,而是想最小化這些分配。這就是說,寫程式時永遠不要把效能作為第一優先順序,因為你並不想(在寫程式時)一直猜測效能。寫正確的代碼才是你第一優先順序。這意味著,我們首先要關注的是完整性、可讀性和簡單性。一旦有了可以啟動並執行程式,才需要確定程式是否足夠快。假如程式不夠快,那麼使用語言提供的工具來尋找和解決效能問題。

via: https://www.ardanlabs.com/blog/2017/06/language-mechanics-on-memory-profiling.html

作者:William Kennedy 譯者:gogeof 校對:polaris1119

本文由 GCTT 原創編譯,Go語言中文網 榮譽推出

本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽

1466 次點擊  ∙  1 贊  

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.