【Go】strings.Replace 与 bytes.Replace 调优

2024-09-02 07:48
文章标签 go replace bytes 调优 strings

本文主要是介绍【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 157flat  flat%   sum%        cum   cum%138.28GB 10.94% 10.94%   138.28GB 10.94%  logrus.(*Entry).WithFields95.27GB  7.54% 18.48%    95.27GB  7.54%  strings.Replace67.05GB  5.31% 23.79%   185.09GB 14.65%  v3.(*v3Adapter).parseEncrypt57.01GB  4.51% 28.30%    57.01GB  4.51%  bufio.NewWriterSize56.63GB  4.48% 32.78%    56.63GB  4.48%  bufio.NewReaderSize56.11GB  4.44% 37.23%    56.11GB  4.44%  net/url.unescape39.75GB  3.15% 40.37%    39.75GB  3.15%  regexp.(*bitState).reset36.11GB  2.86% 43.23%    38.05GB  3.01%  des3_and_base64.(*des3AndBase64).des3Decrypt36.01GB  2.85% 46.08%    36.01GB  2.85%  des3_and_base64.(*des3AndBase64).base64Decode35.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.go95.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 是一个临时变量,可以安全的被引用不用担心,一个小技巧节省一倍的内存分配,但是这样真的就够了吗?

我记得 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 []stringfunc 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 intfor i, j = 0, 0; i < len(s) && j < n; j++ {wid = bytes.Index(s[i:], old)if wid < 0 {break}i += widi += 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 intfor 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 []bytefunc 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 []stringfunc 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 totalflat  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 27flat  flat%   sum%        cum   cum%0.03s  0.25%  0.25%      0.08s  0.65%  strings.Replace0.02s  0.16%  0.41%      0.02s  0.16%  countbody0.01s 0.082%  0.49%      0.01s 0.082%  indexbytebody0.01s 0.082%  0.57%      0.01s 0.082%  memeqbody0.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 18flat  flat%   sum%        cum   cum%0.03s   0.2%   0.2%      0.03s   0.2%  indexbytebody0.02s  0.14%  0.34%      0.05s  0.34%  bytes.Index0.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)

本文链接: https://blog.thinkeridea.com/201902/go/replcae_you_hua.html

版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处!

这篇关于【Go】strings.Replace 与 bytes.Replace 调优的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1129406

相关文章

Hadoop企业开发案例调优场景

需求 (1)需求:从1G数据中,统计每个单词出现次数。服务器3台,每台配置4G内存,4核CPU,4线程。 (2)需求分析: 1G / 128m = 8个MapTask;1个ReduceTask;1个mrAppMaster 平均每个节点运行10个 / 3台 ≈ 3个任务(4    3    3) HDFS参数调优 (1)修改:hadoop-env.sh export HDFS_NAMENOD

Go Playground 在线编程环境

For all examples in this and the next chapter, we will use Go Playground. Go Playground represents a web service that can run programs written in Go. It can be opened in a web browser using the follow

bytes.split的用法和注意事项

当然,我很乐意详细介绍 bytes.Split 的用法和注意事项。这个函数是 Go 标准库中 bytes 包的一个重要组成部分,用于分割字节切片。 基本用法 bytes.Split 的函数签名如下: func Split(s, sep []byte) [][]byte s 是要分割的字节切片sep 是用作分隔符的字节切片返回值是一个二维字节切片,包含分割后的结果 基本使用示例: pa

go基础知识归纳总结

无缓冲的 channel 和有缓冲的 channel 的区别? 在 Go 语言中,channel 是用来在 goroutines 之间传递数据的主要机制。它们有两种类型:无缓冲的 channel 和有缓冲的 channel。 无缓冲的 channel 行为:无缓冲的 channel 是一种同步的通信方式,发送和接收必须同时发生。如果一个 goroutine 试图通过无缓冲 channel

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而

【Go】go连接clickhouse使用TCP协议

离开你是傻是对是错 是看破是软弱 这结果是爱是恨或者是什么 如果是种解脱 怎么会还有眷恋在我心窝 那么爱你为什么                      🎵 黄品源/莫文蔚《那么爱你为什么》 package mainimport ("context""fmt""log""time""github.com/ClickHouse/clickhouse-go/v2")func main(

JVM内存调优原则及几种JVM内存调优方法

JVM内存调优原则及几种JVM内存调优方法 1、堆大小设置。 2、回收器选择。   1、在对JVM内存调优的时候不能只看操作系统级别Java进程所占用的内存,这个数值不能准确的反应堆内存的真实占用情况,因为GC过后这个值是不会变化的,因此内存调优的时候要更多地使用JDK提供的内存查看工具,比如JConsole和Java VisualVM。   2、对JVM内存的系统级的调优主要的目的是减少

Go Select的实现

select语法总结 select对应的每个case如果有已经准备好的case 则进行chan读写操作;若没有则执行defualt语句;若都没有则阻塞当前goroutine,直到某个chan准备好可读或可写,完成对应的case后退出。 Select的内存布局 了解chanel的实现后对select的语法有个疑问,select如何实现多路复用的,为什么没有在第一个channel操作时阻塞 从而导

Go Channel的实现

channel作为goroutine间通信和同步的重要途径,是Go runtime层实现CSP并发模型重要的成员。在不理解底层实现时,经常在使用中对channe相关语法的表现感到疑惑,尤其是select case的行为。因此在了解channel的应用前先看一眼channel的实现。 Channel内存布局 channel是go的内置类型,它可以被存储到变量中,可以作为函数的参数或返回值,它在r

Go 数组赋值问题

package mainimport "fmt"type Student struct {Name stringAge int}func main() {data := make(map[string]*Student)list := []Student{{Name:"a",Age:1},{Name:"b",Age:2},{Name:"c",Age:3},}// 错误 都指向了最后一个v// a