原文链接: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) }