【Go】strings.Replace 与 bytes.Replace 调优 (2)

通过分析我们发现前两个主要是为了删除一个字符串,第三个是为了把一个字符串替换为另一个字符串,并且源数据的生命周期很短暂,在执行替换之后就不再使用了,能不能原地替换字符串呢,原地替换的就会变成零分配了,尝试一下吧。

优化

先写一个函数简单实现原地替换,输入的 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 相当。

修改代码

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wsxsgj.html