【Go】strings.Replace 與 bytes.Replace 調優
原文連結:https://blog.thinkeridea.com/201902/go/replcae_you_hua.html
標準庫中函式大多數情況下更通用,效能並非最好的,還是不能過於迷信標準庫,最近又有了新發現,strings.Replace
這個函式自身的效率已經很好了,但是在特定情況下效率並不是最好的,分享一下我如何優化的吧。
我的服務中有部分程式碼使用 strings.Replace
把一個固定的字串刪除或者替換成另一個字串,它們有幾個特點:
- 舊的字串大於或等於新字串
(len(old) >= len(new)
- 源字串的生命週期很短,替換後就不再使用替換前的字串
- 它們都比較大,往往超過 2k~4k
本博文中使用函式均在 go-extend 中,優化後的函式在 exbytes.Replace 中。
發現問題
近期使用 pprof
分析記憶體分配情況,發現 strings.Replace
排在第二,佔 7.54%
, 分析結果如下:
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
看一下原始碼什麼地方分配的記憶體。
(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
是一個臨時變數,可以安全的被引用不用擔心,一個小技巧節省一倍的記憶體分配,但是這樣真的就夠了嗎?
我記得 bytes
標準庫裡面也有一個 bytes.Replace
方法,如果直接使用這種方法呢就不用重寫一個 strings.Replace
了,使用 go-extend 裡面的兩個魔術方法可以一行程式碼搞定上面的優化效果 s = exbytes.ToString(bytes.Replace(exstrings.UnsafeToBytes(s), []byte{' '}, []byte{''}, -1))
, 雖然是一行程式碼搞定的,但是有點長,exstrings.UnsafeToBytes
方法可以極小的代價把 string
轉成 bytes
, 但是 s
不能是標量或常量字串,必須是執行時產生的字串否者可能導致程式奔潰。
這樣確實減少了一倍的記憶體分配,即使只有 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協議 許可協議。轉載請註明出處!
相關文章
- go效能調優之火焰圖Go
- go dns解析過程及調優GoDNS
- 【效能調優】效能測試、分析與調優基礎
- Nginx安全優化與效能調優Nginx優化
- Go~介紹與優勢Go
- MySQL效能診斷與調優MySql
- 探探Java之 JVM GC與調優JavaJVMGC
- JVM效能調優與實戰篇JVM
- Go 實現 Raft 第四篇:持久化和調優GoRaft持久化
- Spark 效能調優--資源調優Spark
- JVM調優總結-調優方法JVM
- Spark 效能調優--Shuffle調優 SortShuffleManagerSpark
- Buffer cache 的調整與優化(二)優化
- Buffer cache 的調整與優化(一)優化
- 【sql調優】繫結變數與CBOSQL變數
- 【Spark篇】---Spark調優之程式碼調優,資料本地化調優,記憶體調優,SparkShuffle調優,Executor的堆外記憶體調優Spark記憶體
- 調優 | Apache Hudi應用調優指南Apache
- JVM調優總結(十)-調優方法JVM
- Redis 基礎、高階特性與效能調優Redis
- Redis基礎、高階特性與效能調優Redis
- spark task與stage數量過多調優Spark
- 效能測試之測試分析與調優
- Linux伺服器效能分析與調優Linux伺服器
- tomcat配置調優與安全總結(轉)Tomcat
- 【sql調優】cardinality測試與簡析SQL
- Linux優化之IO子系統監控與調優Linux優化
- 軟體效能測試分析與調優實踐之路-Java應用程式的效能分析與調優-手稿節選Java
- MySQL調優篇 | SQL調優實戰(5)MySql
- 效能調優學習之硬體調優
- SparkSQL 調優SparkSQL
- php調優PHP
- 模型調優模型
- JVM調優JVM
- MySQL調優MySql
- SQL調優SQL
- 效能調優-Mysql索引資料結構詳解與索引優化MySql索引資料結構優化
- MySql(七):MySQL效能調優——鎖定機制與鎖優化分析MySql優化
- 數倉調優實戰:GUC引數調優