原文連接:https://blog.thinkeridea.com/...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
是一個臨時變量,能夠安全的被引用不用擔憂,一個小技巧節省一倍的內存分配,可是這樣真的就夠了嗎?app
我記得 bytes
標準庫裏面也有一個 bytes.Replace
方法,若是直接使用這種方法呢就不用重寫一個 strings.Replace
了,使用 go-extend 裏面的兩個魔術方法能夠一行代碼搞定上面的優化效果 s = exbytes.ToString(bytes.Replace(exstrings.UnsafeToBytes(s), []byte{' '}, []byte{''}, -1))
, 雖然是一行代碼搞定的,可是有點長,exstrings.UnsafeToBytes
方法能夠極小的代價把 string
轉成 bytes
, 可是 s
不能是標量或常量字符串,必須是運行時產生的字符串否者可能致使程序奔潰。ide
這樣確實減小了一倍的內存分配,即便只有 47.46GB
的分配也足以排到前十了,不滿意這個結果,分析代碼看看能不能更進一步減小內存分配吧。函數
使用火焰圖看看究竟什麼函數在調用 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)
本文連接: https://blog.thinkeridea.com/201902/go/replcae_you_hua.html
版權聲明: 本博客全部文章除特別聲明外,均採用 CC BY 4.0 CN協議 許可協議。轉載請註明出處!