在前一篇博客中,我介紹了逃逸分析的基礎場景。可是還有一些其餘場景,我並無作介紹。爲了介紹其餘場景,我專門寫了了一個程序用於 debug,這個程序中分配內存的方式比較讓人吃驚。正則表達式
爲了更多的學習io
包,我嘗試了一個快速的項目。找到字節流中的字符串 elvis
,而且替換爲首字母大寫的字符串 Elvis
。算法
代碼中列出了兩個用於解決這個這個問題的函數。這個博客主要集中於函數algOne
,由於這個函數用到了io
包。shell
下面的數據中,一個是輸入,一個是但願經過函數algOne
做用以後的輸出。數組
Listing 1bash
Input:
abcelvisaElvisabcelviseelvisaelvisaabeeeelvise l v i saa bb e l v i saa elvi
selvielviselvielvielviselvi1elvielviselvis
Output:
abcElvisaElvisabcElviseElvisaElvisaabeeeElvise l v i saa bb e l v i saa elvi
selviElviselvielviElviselvi1elviElvisElvis
複製代碼
下面是函數algOne
ide
Listing 2函數
80 func algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) {
81
82 // Use a bytes Buffer to provide a stream to process.
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.
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 {
94 output.Write(buf[:n])
95 return
96 }
97
98 for {
99
100 // Read in one byte from the input stream.
101 if _, err := io.ReadFull(input, buf[end:]); err != nil {
102
103 // Flush the reset of the bytes we have.
104 output.Write(buf[:end])
105 return
106 }
107
108 // If we have a match, replace the bytes.
109 if bytes.Compare(buf, find) == 0 {
110 output.Write(repl)
111
112 // Read a new initial number of bytes.
113 if n, err := io.ReadFull(input, buf[:end]); err != nil {
114 output.Write(buf[:n])
115 return
116 }
117
118 continue
119 }
120
121 // Write the front byte since it has been compared.
122 output.WriteByte(buf[0])
123
124 // Slice that front byte out.
125 copy(buf, buf[1:])
126 }
127 }
複製代碼
我想知道這個函數的表現以及函數給堆上的壓力。爲了瞭解這些,咱們須要運行下 benchmark。工具
下面是用來運行函數algOne
來處流數據的 benchmark 函數性能
Listing 3學習
15 func BenchmarkAlgorithmOne(b *testing.B) {
16 var output bytes.Buffer
17 in := assembleInputStream()
18 find := []byte("elvis")
19 repl := []byte("Elvis")
20
21 b.ResetTimer()
22
23 for i := 0; i < b.N; i++ {
24 output.Reset()
25 algOne(in, find, repl, &output)
26 }
27 }
複製代碼
有了這個函數,咱們就能夠運行go test
了,而且可使用選項-bench
,-benchtime
和-benchmem
選項。
Listing 4
$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem
BenchmarkAlgorithmOne-8 2000000 2522 ns/op 117 B/op 2 allocs/op
複製代碼
在運行 benchmark 以後,咱們能夠看到函數algOne
函數的每次操做都分配了兩次內存,而且分配的內存大小爲 117 字節。這個表現很是好了,可是咱們須要知道是哪些代碼形成了這些內存的分配。爲了知道這些,咱們須要產生運行 benchmark 的 profiling data。
爲了產生 profile data,咱們須要運行 benchmark,不過此次須要使用選項 -memprofile
選項。
Listing 5
$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out
BenchmarkAlgorithmOne-8 2000000 2570 ns/op 117 B/op 2 allocs/op
複製代碼
在程序運行完以後,就會產生兩個新的文件。
Listing 6
~/code/go/src/.../memcpu
$ ls -l
total 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
中,函數BenchmarkAlgorithmOne
存在於stream_test.go
。兩個產生的文件分別是mem.out
和memcpu.test
。文件mem.out
包含了 profiles data。文件memcpu.test
是一個二進制文件,當咱們須要看 profile data 的時候須要使用到這個文件。
有了 profile data 和二進制文件,咱們就能夠運行pprof
工具來學習 profile data。
Listing 7
$ go tool pprof -alloc_space memcpu.test mem.out
Entering interactive mode (type "help" for commands)
(pprof) _
複製代碼
當須要 profiling memory 而且尋找容易解決的問題的時候,咱們須要使用選項-alloc_space
而不是默認的選項-inuse_space
。這個選項會展現每次分配內存的狀況,而無論你 take the profile 的時候,分配的內存是否還在使用。
經過pprof
的做用,咱們可使用list
命令來檢查函數algOne
的狀況。list
命令接受一個正則表達式,用於匹配表達式匹配的函數。
Listing 8
(pprof) list algOne
Total: 335.03MB
ROUTINE ======================== .../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) _
複製代碼
基於這個 profile, 咱們能夠知道input
以及切片buf
的底層數組被分配到了堆。因爲input
是指針,因此這個 profile 是說明,input
所指向的bytes.Buffer
是分配的到堆的。因此咱們先聚焦於變量input
的變量的分配,而且理解是如何分配的。
因爲函數bytes.NewBuffer
建立的變量,和函數algOne
共享,因此致使變量分配到堆。而且flat
列(pprof 輸出的第一列)出現的值告訴咱們這個值是分配到堆的,由於函數algOne
共享變量的緣由致使的變量分配逃逸到堆。
flat
列表示的是函數的堆的分配,能夠看看list
命令展現函數Benchmark
是如何調用函數algOne
的。
Listing 9
(pprof) list Benchmark
Total: 335.03MB
ROUTINE ======================== .../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
命令調用的時候,分配的值到堆是匹配的(譯者注:$$318.53 + 16.50 = 335.03$$)。
到此呢,咱們仍然不知道爲何bytes.Buffer
會建立變量到堆。這個時候可使用go build
命令的-gcflags "-m -m"
選項了。profiler
會告訴咱們值逃逸到的堆,而go build
命令會告訴咱們爲何。
咱們可讓編譯器告訴咱們代碼裏面變量逃逸到堆的緣由。
Listing 10
$ go build -gcflags "-m -m"
複製代碼
這個命令會產生很是多的輸出。咱們須要找到的就是包含stream.go:83
的行,由於stream.go
是文件的名稱,而且第 83 行含有代碼來構建bytes.buffer
的值。在搜索以後,找到了以下 6 行。
Listing 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
複製代碼
第一行是很是有意思的
Listing 12
./stream.go:83: inlining call to bytes.NewBuffer func([]byte) *bytes.Buffer { return &bytes.Buffer literal }
複製代碼
這句話告訴了咱們bytes.Buffer
逃逸到堆的緣由並非對函數bytes.Buffer
調用形成的。由於bytes.Buffer
壓根沒有被調用,函數的操做被內聯到了調用的地方。
第 83 行的的以下代碼
Listing 13
83 input := bytes.NewBuffer(data)
複製代碼
因爲編譯器選擇把bytes.NewBuffer
內聯到代碼裏面,因此上面的代碼在實際調用的時候是以下的
Listing 14
input := &bytes.Buffer{buf: data}
複製代碼
這就意味着函數algOne
是直接建立bytes.Buffer
的。那麼究竟是什麼致使 input 被分配到堆中的呢?答案就在剩下的五行報告中。
Listing 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
變量被賦值給了一個接口。
我並無印象在代碼中對接口有過賦值的操做。可是若是看了第 93 行代碼,問題就變得清晰了。
Listing 16
93 if n, err := io.ReadFull(input, buf[:end]); err != nil {
94 output.Write(buf[:n])
95 return
96 }
複製代碼
因爲調用了io.ReadFull
函數,因此形成了對接口的賦值。若是你看了io.ReadFull
的定義,你能夠看到函數io.ReadFull
接受的第一個參數是一個接口。
Listing 17
type 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
的地址傳遞給函數,而後函數把這個地址做爲一個接口存儲,這就形成了變量逃逸到了堆。如今咱們看到了使用接口的代價:變量分配到堆和變量的間接使用(若是分配到棧,變量的訪問速度會更快)。若是使用接口並無使得代碼變得更好,那就最好別使用接口。我跟隨這下面這些指導來使用接口
當有下面幾種狀況的時候,我會使用接口
不須要使用接口的狀況以下
如今咱們須要問本身,這個算法真的須要使用io.ReadFull
函數嗎?答案是否認的,由於bytes.Buffer
類型有一系列方法可使用,而且使用這些方法能夠有效的避免變量被分配到堆。
如今咱們能夠移去io
包,並使用input
變量已有的方法Read
。
下面的代碼移去了io
包,爲了保持新的代碼行和原來的代碼行不變,使用了變量_
來避免導入io
包。這樣就能夠保持io
包還在引入的行列中。
Listing 18
12 import (
13 "bytes"
14 "fmt"
15 _ "io"
16 )
80 func algOne(data []byte, find []byte, repl []byte, output *bytes.Buffer) {
81
82 // Use a bytes Buffer to provide a stream to process.
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.
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])
95 return
96 }
97
98 for {
99
100 // Read in one byte from the input stream.
101 if _, err := input.Read(buf[end:]); err != nil {
102
103 // Flush the reset of the bytes we have.
104 output.Write(buf[:end])
105 return
106 }
107
108 // If we have a match, replace the bytes.
109 if bytes.Compare(buf, find) == 0 {
110 output.Write(repl)
111
112 // Read a new initial number of bytes.
113 if n, err := input.Read(buf[:end]); err != nil || n < end {
114 output.Write(buf[:n])
115 return
116 }
117
118 continue
119 }
120
121 // Write the front byte since it has been compared.
122 output.WriteByte(buf[0])
123
124 // Slice that front byte out.
125 copy(buf, buf[1:])
126 }
127 }
複製代碼
當咱們再次運行 benchmark 的時候,就能夠看到變量bytes.Buffer
再也不分配到堆中了。
Listing 19
$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem -memprofile mem.out
BenchmarkAlgorithmOne-8 2000000 1814 ns/op 5 B/op 1 allocs/op
複製代碼
也能夠從上面的輸出看到,代碼性能提高了約 29%。代碼花費的時間由 2570 ns/op 到 1814 ns/op。既然這個問題解決了,咱們如今就能夠聚焦於切片buf
背後的數組分配到了堆的問題。若是咱們使用新的代碼,來運行獲得 profile 的結果,咱們也許就能夠解決這個問題了。
Listing 20
$ go tool pprof -alloc_space memcpu.test mem.out
Entering interactive mode (type "help" for commands)
(pprof) list algOne
Total: 7.50MB
ROUTINE ======================== .../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
。
Listing 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 到代碼中去
Listing 22
89 buf := make([]byte, 5)
複製代碼
這個時候再運行 benchmark,全部的分配到堆的操做都沒有了。
Listing 23
$ go test -run none -bench AlgorithmOne -benchtime 3s -benchmem
BenchmarkAlgorithmOne-8 3000000 1720 ns/op 0 B/op 0 allocs/op
複製代碼
若是再次查看編譯器的報告,你會發現沒有變量的逃逸行爲
Listing 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
複製代碼
顯然,並不能硬編碼切片的大小到代碼中,因此代碼中知道存在着一次的變量分配到堆的操做。
有了三次的修改,咱們能夠查看、對比每次修改後的性能
Listing 25
Before any optimization
BenchmarkAlgorithmOne-8 2000000 2570 ns/op 117 B/op 2 allocs/op
Removing the bytes.Buffer allocation
BenchmarkAlgorithmOne-8 2000000 1814 ns/op 5 B/op 1 allocs/op
Removing the backing array allocation
BenchmarkAlgorithmOne-8 3000000 1720 ns/op 0 B/op 0 allocs/op
複製代碼
在第一優化的時候,性能提高大約 29%。第二次優化以後,性能提高約 33%。經過這些數據,咱們能夠看到變量分配到堆是影響程序性能的。
go 有許多讓人吃驚的工具,來讓咱們理解編譯器在涉及到逃逸分析是所作的決定的起因。基於這些信息,咱們能夠修改代碼以保持能夠存在於棧中的變量避免存在於堆中。你並不須要完成一個在堆上不分配內存的程序,可是你須要使得這些操做盡量的避免。
永遠不要基於程序的性能寫代碼,由於你不想猜想程序的性能。咱們應該首先基於正確性來寫代碼。這意味着須要聚焦於總體性,可讀性和簡單性。在你有了一個程序的時候,確認下程序是否運行的足夠塊。若是不夠快,那麼可使用 go 提供的工具來找到修復程序運行慢的問題。