【Go】strings.Replace 與 bytes.Replace 調優

原文連接:blog.thinkeridea.com/201902/go/r…html

標準庫中函數大多數狀況下更通用,性能並不是最好的,仍是不能過於迷信標準庫,最近又有了新發現,strings.Replace 這個函數自身的效率已經很好了,可是在特定狀況下效率並非最好的,分享一下我如何優化的吧。node

個人服務中有部分代碼使用 strings.Replace 把一個固定的字符串刪除或者替換成另外一個字符串,它們有幾個特色:git

  • 舊的字符串大於或等於新字符串 (len(old) >= len(new)
  • 源字符串的生命週期很短,替換後就再也不使用替換前的字符串
  • 它們都比較大,每每超過 2k~4k

本博文中使用函數均在 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,這個函數能夠零分配轉換 []bytestringt 是一個臨時變量,能夠安全的被引用不用擔憂,一個小技巧節省一倍的內存分配,可是這樣真的就夠了嗎?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 呢:

image-20190202184040233

這裏主要是兩個方法在使用,固然我記得還有幾個地方有使用,看來不在火焰圖中應該影響比較低 ,看一下代碼吧(簡化的代碼不必定徹底合理):

// 第一部分
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.Replacebytes.Replace 優化圓滿結束。

本博文中使用函數均在 go-extend 中,優化後的函數在 exbytes.Replace 中。

轉載:

本文做者: 戚銀(thinkeridea

本文連接: blog.thinkeridea.com/201902/go/r…

版權聲明: 本博客全部文章除特別聲明外,均採用 CC BY 4.0 CN協議 許可協議。轉載請註明出處!

相關文章
相關標籤/搜索