原文連接:blog.thinkeridea.com/201902/go/r…html
標準庫中函數大多數狀況下更通用,性能並不是最好的,仍是不能過於迷信標準庫,最近又有了新發現,strings.Replace
這個函數自身的效率已經很好了,可是在特定狀況下效率並非最好的,分享一下我如何優化的吧。node
個人服務中有部分代碼使用 strings.Replace
把一個固定的字符串刪除或者替換成另外一個字符串,它們有幾個特色:git
(len(old) >= len(new)
本博文中使用函數均在 go-extend 中,優化後的函數在 exbytes.Replace 中。github
近期使用 pprof
分析內存分配狀況,發現 strings.Replace
排在第二,佔 7.54%
, 分析結果以下:shell
go tool pprof allocs
File: xxx
Type: alloc_space
Time: Feb 1, 2019 at 9:53pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top
Showing nodes accounting for 617.29GB, 48.86% of 1263.51GB total
Dropped 778 nodes (cum <= 6.32GB)
Showing top 10 nodes out of 157
flat flat% sum% cum cum%
138.28GB 10.94% 10.94% 138.28GB 10.94% logrus.(*Entry).WithFields
95.27GB 7.54% 18.48% 95.27GB 7.54% strings.Replace
67.05GB 5.31% 23.79% 185.09GB 14.65% v3.(*v3Adapter).parseEncrypt
57.01GB 4.51% 28.30% 57.01GB 4.51% bufio.NewWriterSize
56.63GB 4.48% 32.78% 56.63GB 4.48% bufio.NewReaderSize
56.11GB 4.44% 37.23% 56.11GB 4.44% net/url.unescape
39.75GB 3.15% 40.37% 39.75GB 3.15% regexp.(*bitState).reset
36.11GB 2.86% 43.23% 38.05GB 3.01% des3_and_base64.(*des3AndBase64).des3Decrypt
36.01GB 2.85% 46.08% 36.01GB 2.85% des3_and_base64.(*des3AndBase64).base64Decode
35.08GB 2.78% 48.86% 35.08GB 2.78% math/big.nat.make
複製代碼
標準庫中最經常使用的函數,竟然……,不可忍必須優化,先使用 list strings.Replace
看一下源碼什麼地方分配的內存。express
(pprof) list strings.Replace
Total: 1.23TB
ROUTINE ======================== strings.Replace in /usr/local/go/src/strings/strings.go
95.27GB 95.27GB (flat, cum) 7.54% of Total
. . 858: } else if n < 0 || m < n {
. . 859: n = m
. . 860: }
. . 861:
. . 862: // Apply replacements to buffer.
47.46GB 47.46GB 863: t := make([]byte, len(s)+n*(len(new)-len(old)))
. . 864: w := 0
. . 865: start := 0
. . 866: for i := 0; i < n; i++ {
. . 867: j := start
. . 868: if len(old) == 0 {
. . 869: if i > 0 {
. . 870: _, wid := utf8.DecodeRuneInString(s[start:])
. . 871: j += wid
. . 872: }
. . 873: } else {
. . 874: j += Index(s[start:], old)
. . 875: }
. . 876: w += copy(t[w:], s[start:j])
. . 877: w += copy(t[w:], new)
. . 878: start = j + len(old)
. . 879: }
. . 880: w += copy(t[w:], s[start:])
47.81GB 47.81GB 881: return string(t[0:w])
. . 882:}
複製代碼
從源碼發現首先建立了一個 buffer
來起到緩衝的效果,一次分配足夠的內存,這個在以前 【Go】slice的一些使用技巧 裏面有講到,另一個是 string(t[0:w])
類型轉換帶來的內存拷貝,buffer
可以理解,可是類型轉換這個不能忍,就像憑空多出來的一個數拷貝。安全
既然類型轉換這裏有點浪費空間,有沒有辦法能夠零成本轉換呢,那就使用 go-extend 這個包裏面的 exbytes.ToString
方法把 []byte
轉換成 string
,這個函數能夠零分配轉換 []byte
到 string
。 t
是一個臨時變量,能夠安全的被引用不用擔憂,一個小技巧節省一倍的內存分配,可是這樣真的就夠了嗎?bash
我記得 bytes
標準庫裏面也有一個 bytes.Replace
方法,若是直接使用這種方法呢就不用重寫一個 strings.Replace
了,使用 go-extend 裏面的兩個魔術方法能夠一行代碼搞定上面的優化效果 s = exbytes.ToString(bytes.Replace(exstrings.UnsafeToBytes(s), []byte{' '}, []byte{''}, -1))
, 雖然是一行代碼搞定的,可是有點長,exstrings.UnsafeToBytes
方法能夠極小的代價把 string
轉成 bytes
, 可是 s
不能是標量或常量字符串,必須是運行時產生的字符串否者可能致使程序奔潰。app
這樣確實減小了一倍的內存分配,即便只有 47.46GB
的分配也足以排到前十了,不滿意這個結果,分析代碼看看能不能更進一步減小內存分配吧。ide
使用火焰圖看看究竟什麼函數在調用 strings.Replace
呢:
這裏主要是兩個方法在使用,固然我記得還有幾個地方有使用,看來不在火焰圖中應該影響比較低 ,看一下代碼吧(簡化的代碼不必定徹底合理):
// 第一部分
func (v2 *v2Adapter) parse(s string) (*AdRequest, error) {
s = strings.Replace(s, " ", "", -1)
requestJSON, err := v2.paramCrypto.Decrypt([]byte(s))
if err != nil {
return nil, err
}
request := v2.getDefaultAdRequest()
if err := request.UnmarshalJSON(requestJSON); err != nil {
return nil, err
}
return request, nil
}
// 第二部分
func (v3 *v3Adapter) parseEncrypt(s []byte) ([]byte, error) {
ss := strings.Replace(string(s), " ", "", -1)
requestJSON, err := v3.paramCrypto.Decrypt([]byte(ss))
if err != nil {
return nil, error
}
return requestJSON, nil
}
// 經過搜索找到的第三部分
type LogItems []string
func LogItemsToBytes(items []string, sep, newline string) []byte {
for i := range items {
items[i] = strings.Replace(items[i], sep, " ", -1)
}
str := strings.Replace(strings.Join(items, sep), newline, " ", -1)
return []byte(str + newline)
}
複製代碼
經過分析咱們發現前兩個主要是爲了刪除一個字符串,第三個是爲了把一個字符串替換爲另外一個字符串,而且源數據的生命週期很短暫,在執行替換以後就再也不使用了,能不能原地替換字符串呢,原地替換的就會變成零分配了,嘗試一下吧。
先寫一個函數簡單實現原地替換,輸入的 len(old) < len(new)
就直接調用 bytes.Replace
來實現就行了 。
func Replace(s, old, new []byte, n int) []byte {
if n == 0 {
return s
}
if len(old) < len(new) {
return bytes.Replace(s, old, new, n)
}
if n < 0 {
n = len(s)
}
var wid, i, j int
for i, j = 0, 0; i < len(s) && j < n; j++ {
wid = bytes.Index(s[i:], old)
if wid < 0 {
break
}
i += wid
i += copy(s[i:], new)
s = append(s[:i], s[i+len(old)-len(new):]...)
}
return s
}
複製代碼
寫個性能測試看一下效果:
$ go test -bench="." -run=nil -benchmem
goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exbytes/benchmark
BenchmarkReplace-8 500000 3139 ns/op 416 B/op 1 allocs/op
BenchmarkBytesReplace-8 1000000 2032 ns/op 736 B/op 2 allocs/op
複製代碼
使用這個新的函數和 bytes.Replace
對比,內存分配是少了,可是性能卻降低了那麼多,崩潰.... 啥狀況呢,對比 bytes.Replace
的源碼發現我這個代碼裏面 s = append(s[:i], s[i+len(old)-len(new):]...)
每次都會移動剩餘的數據致使性能差別很大,可使用 go test -bench="." -run=nil -benchmem -cpuprofile cpu.out -memprofile mem.out
的方式來生成 pprof
數據,而後分析具體有問題的地方。
找到問題就行了,移動 wid
以前的數據,這樣每次移動就不多了,和 bytes.Replace
的原理相似。
func Replace(s, old, new []byte, n int) []byte {
if n == 0 {
return s
}
if len(old) < len(new) {
return bytes.Replace(s, old, new, n)
}
if n < 0 {
n = len(s)
}
var wid, i, j, w int
for i, j = 0, 0; i < len(s) && j < n; j++ {
wid = bytes.Index(s[i:], old)
if wid < 0 {
break
}
w += copy(s[w:], s[i:i+wid])
w += copy(s[w:], new)
i += wid + len(old)
}
w += copy(s[w:], s[i:])
return s[0:w]
}
複製代碼
在運行一下性能測試吧:
$ go test -bench="." -run=nil -benchmem
goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exbytes/benchmark
BenchmarkReplace-8 1000000 2149 ns/op 416 B/op 1 allocs/op
BenchmarkBytesReplace-8 1000000 2231 ns/op 736 B/op 2 allocs/op
複製代碼
運行性能差很少,並且更好了,內存分配也減小,不是說是零分配嗎,爲啥有一次分配呢?
var replaces string
var replaceb []byte
func init() {
replaces = strings.Repeat("A BC", 100)
replaceb = bytes.Repeat([]byte("A BC"), 100)
}
func BenchmarkReplace(b *testing.B) {
for i := 0; i < b.N; i++ {
exbytes.Replace([]byte(replaces), []byte(" "), []byte(""), -1)
}
}
func BenchmarkBytesReplace(b *testing.B) {
for i := 0; i < b.N; i++ {
bytes.Replace([]byte(replaces), []byte(" "), []byte(""), -1)
}
}
複製代碼
能夠看到使用了 []byte(replaces)
作了一次類型轉換,由於優化的這個函數是原地替換,執行過一次以後後面就發現不用替換了,因此爲了公平公正兩個方法每次都轉換一個類型產生一個新的內存地址,因此實際優化後是沒有內存分配了。
以前說寫一個優化 strings.Replace
函數,減小一次內存分配,這裏也寫一個這樣函數,而後增長兩個性能測試函數,對比一下效率 性能測試代碼:
$ go test -bench="." -run=nil -benchmem
goos: darwin
goarch: amd64
pkg: github.com/thinkeridea/go-extend/exbytes/benchmark
BenchmarkReplace-8 1000000 2149 ns/op 416 B/op 1 allocs/op
BenchmarkBytesReplace-8 1000000 2231 ns/op 736 B/op 2 allocs/op
BenchmarkStringsReplace-8 1000000 2260 ns/op 1056 B/op 3 allocs/op
BenchmarkUnsafeStringsReplace-8 1000000 2522 ns/op 736 B/op 2 allocs/op
PASS
ok github.com/thinkeridea/go-extend/exbytes/benchmark 10.260s
複製代碼
運行效率上都至關,優化以後的 UnsafeStringsReplace
函數減小了一次內存分配只有一次,和 bytes.Replace
至關。
有了優化版的 Replace
函數就替換到項目中吧:
// 第一部分
func (v2 *v2Adapter) parse(s string) (*AdRequest, error) {
b := exbytes.Replace(exstrings.UnsafeToBytes(s), []byte(" "), []byte(""), -1)
requestJSON, err := v2.paramCrypto.Decrypt(b)
if err != nil {
return nil, err
}
request := v2.getDefaultAdRequest()
if err := request.UnmarshalJSON(requestJSON); err != nil {
return nil, err
}
return request, nil
}
// 第二部分
func (v3 *v3Adapter) parseEncrypt(s []byte) ([]byte, error) {
s = exbytes.Replace(s, []byte(" "), []byte(""), -1)
requestJSON, err := v3.paramCrypto.Decrypt(s)
if err != nil {
return nil, err
}
return requestJSON, nil
}
// 第三部分
type LogItems []string
func LogItemsToBytes(items []string, sep, newline string) []byte {
for i := range items {
items[i] = exbytes.ToString(exbytes.Replace(exstrings.UnsafeToBytes(items[i]), []byte(sep), []byte(" "), -1))
}
b := exbytes.Replace(exstrings.UnsafeToBytes(strings.Join(items, sep)), []byte(newline), []byte(" "), -1)
return append(b, newline...)
}
複製代碼
$ go tool pprof allocs2
File: xx
Type: alloc_space
Time: Feb 2, 2019 at 5:33pm (CST)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top exbytes.Replace
Focus expression matched no samples
Active filters:
focus=exbytes.Replace
Showing nodes accounting for 0, 0% of 864.21GB total
flat flat% sum% cum cum%
(pprof)
複製代碼
竟然在 allocs
上竟然找不到了,確實是零分配。
優化前 profile
:
$ go tool pprof profile
File: xx
Type: cpu
Time: Feb 1, 2019 at 9:54pm (CST)
Duration: 30.08s, Total samples = 12.23s (40.65%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top strings.Replace
Active filters:
focus=strings.Replace
Showing nodes accounting for 0.08s, 0.65% of 12.23s total
Showing top 10 nodes out of 27
flat flat% sum% cum cum%
0.03s 0.25% 0.25% 0.08s 0.65% strings.Replace
0.02s 0.16% 0.41% 0.02s 0.16% countbody
0.01s 0.082% 0.49% 0.01s 0.082% indexbytebody
0.01s 0.082% 0.57% 0.01s 0.082% memeqbody
0.01s 0.082% 0.65% 0.01s 0.082% runtime.scanobject
複製代碼
優化後 profile
:
$ go tool pprof profile2
File: xx
Type: cpu
Time: Feb 2, 2019 at 5:33pm (CST)
Duration: 30.16s, Total samples = 14.68s (48.68%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top exbytes.Replace
Active filters:
focus=exbytes.Replace
Showing nodes accounting for 0.06s, 0.41% of 14.68s total
Showing top 10 nodes out of 18
flat flat% sum% cum cum%
0.03s 0.2% 0.2% 0.03s 0.2% indexbytebody
0.02s 0.14% 0.34% 0.05s 0.34% bytes.Index
0.01s 0.068% 0.41% 0.06s 0.41% github.com/thinkeridea/go-extend/exbytes.Replace
複製代碼
經過 profile
來分配發現性能也有必定的提高,本次 strings.Replace
和 bytes.Replace
優化圓滿結束。
本博文中使用函數均在 go-extend 中,優化後的函數在 exbytes.Replace 中。
轉載:
本文做者: 戚銀(thinkeridea)
本文連接: blog.thinkeridea.com/201902/go/r…
版權聲明: 本博客全部文章除特別聲明外,均採用 CC BY 4.0 CN協議 許可協議。轉載請註明出處!