原文链接: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,这个函数可以零分配转换 []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 []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

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

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

【Go】strings.Replace 与 bytes.Replace 调优的更多相关文章

  1. 16-MySQL DBA笔记-调优基础理论和工具

    第五部分 性能调优与架构篇 本篇将为读者介绍性能调优的一些背景知识和理论,然后介绍一些工具的运用,最后介绍从应用程序到操作系统.到数据库.到存储各个环节的优化. 性能调优是一个高度专业的领域,它需要一 ...

  2. 类加载机制+JVM调优实战+代码优化

    类加载机制 Java源代码经过编译器编译成字节码之后,最终都需要加载到虚拟机之后才能运行.虚拟机把描述类的数据从 Class 文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直 ...

  3. Oracle调优总结(经典实践 重要)

    转载:http://langgufu.iteye.com/blog/1974211 Problem Description:1.每个表的结构及主键索引情况2.每个表的count(*)记录是多少3.对于 ...

  4. (转)调优 DB2 UDB v8.1 及其数据库的最佳实践

    原文:https://www.ibm.com/developerworks/cn/data/library/techarticles/dm-0404mcarthur/index.html 简介 性能是 ...

  5. MySQL性能调优与架构设计——第2章 MySQL架构组成

    第2章 MySQL架构组成 前言   麻雀虽小,五脏俱全.MySQL 虽然以简单著称,但其内部结构并不简单.本章从MySQL物理组成.逻辑组成,以及相关工具几个角度来介绍 MySQL 的整体架构组成, ...

  6. 【oracle11g,13】表空间管理2:undo表空间管理(调优) ,闪回原理

    一.undo空间原理: dml操作会产生undo数据. update时,sever process 会在databuffer 中找到该记录的buffer块,没有就从datafile中找并读入data ...

  7. advisor调优工具优化sql(基于sql_id)

    advisor调优工具优化sql(基于sql_id) 问题背景:客户反馈数据库迁移后cpu负载激增,帮忙查看原因 解决思路:1> 查看问题系统发现有大量的latch: cache buffers ...

  8. Impala 架构探索-Impala 系统组成与使用调优

    要好好使用 Impala 就得好好梳理一下他得结构以及他存在得一些问题或者需要注意得地方.本系列博客主要想记录一下对 Impala 架构梳理以及使用上的 workaround. Impala 简介 首 ...

  9. jvm系列(四):jvm调优-命令大全(jps jstat jmap jhat jstack jinfo)

    文章同步发布于github博客地址,阅读效果更佳,欢迎品尝 运用jvm自带的命令可以方便的在生产监控和打印堆栈的日志信息帮忙我们来定位问题!虽然jvm调优成熟的工具已经有很多:jconsole.大名鼎 ...

随机推荐

  1. Codeforces Avito Code Challenge 2018 D. Bookshelves

    Codeforces Avito Code Challenge 2018 D. Bookshelves 题目连接: http://codeforces.com/contest/981/problem/ ...

  2. [swarthmore cs75] Compiler 4 – Diamondback

    课程回顾 Swarthmore学院16年开的编译系统课,总共10次大作业.本随笔记录了相关的课堂笔记以及第6次大作业. 函数声明 增加函数声明.函数调用的抽象语法:在转换成anf之前还要检查函数声明和 ...

  3. java面试问题收集(2)

    1 Integer int相等问题 Integer对象和int比较的时候会有一个拆箱的过程,始终相等 Integer和new Integer对象不会相等,引用不同 两个Integer对象比较,Inte ...

  4. shell脚本新建文件夹或用到目录时多出M或者?之类的

    新建问价加多出? 删除多显示M   建立软连接多\n等 可能是文件兼容问题, 1.首先用vi命令打开文件[root@localhost test]# vi test.sh   2.在vi命令模式中使用 ...

  5. Asp.net Identity框架

    Identity提供基于用户和角色的membership管理框架,基本上可以满足业务项目登录操作的所有功能需求. 如果要使用这套框架需要新建User和Role类型分别继承自IUser<TKey& ...

  6. QEMU KVM Libvirt手册(5) – snapshots

    前面讲了QEMU的qcow2格式的internal snapshot和external snapshot,这都是虚拟机文件格式的功能. 这是文件级别的. 还可以是文件系统级别的,比如很多文件系统支持s ...

  7. 《JavaScript》高级程序设计第21章:Ajax和Comet,jsonp

    一.创建XMLHttpRequest对象 二.XHR的用法 五.跨域资源共享 六.其他跨域技术七.安全七.安全 1. 图像Ping 2. JSONP(JSON with padding,填充式JSON ...

  8. 如何利用sqoop将hive数据导入导出数据到mysql

    运行环境  centos 5.6   hadoop  hive sqoop是让hadoop技术支持的clouder公司开发的一个在关系数据库和hdfs,hive之间数据导入导出的一个工具. 上海尚学堂 ...

  9. HTML页面中插入CSS样式的三种方法

    1. 外部样式 当样式需要应用于很多页面时,外部样式表将是理想的选择.在使用外部样式表的情况下,你可以通过改变一个文件来改变整个站点的外观.每个页面使用<link>标签链接到样式表. &l ...

  10. Web前端-Vue.js必备框架(二)

    Web前端-Vue.js必备框架(二) vue调式工具vue-devtools 过滤器:vue.js允许你自定义过滤器,可被用作一些常见的文本格式化. mustache插值和v-bind表达式. vu ...