【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 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协议 许可协议。转载请注明出处!

原文地址:https://www.cnblogs.com/thinkeridea/p/10350228.html

时间: 2024-10-23 15:47:56

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

通过案例学调优之--SQL Profile

通过案例学调优之--SQL Profile 一.什么是SQL Profile(概要) SQL Profile在性能优化中占有一个重要的位置. MOS里这么描述SQL Profile: SQL Profile是10g中的新特性,作为自动SQL调整过程的一部分,由Oracle企业管理器来管理.除了OEM,SQL Profile可以通过DBMS_SQLTUNE包来进行管理. 查询优化器有时候会因为缺乏足够的信息,而对一条SQL语句做出错误的估计,生成糟糕的执行计划.而自动SQL调整通过SQL概要分析来

SQL自动调优

SQL自动调优,是oracle 自带的调优工具,可以提出一些解决方案. 本次我主要介绍下面这些自动SQL调优工具: 自动SQL调优(automatic sql tuning) SQL调优工具集(SQL tuning sets,STS) SQL调优顾问(SQL Tuning Advisor) 自动数据库诊断监视器(addm) 显示SQL自动调优建议最快的方法: SQL> select dbms_auto_sqltune.report_auto_tuning_task from dual; GENE

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

一.undo空间原理: dml操作会产生undo数据. update时,sever process 会在databuffer 中找到该记录的buffer块,没有就从datafile中找并读入data buffer.在修改之前,原始数据先放到undo段,并在数据块头记录undo段(acitve 状态)中该数据块的位置,读写这个块时会占用事务槽,会将该事务号记录在数据块的头部.然后在进行update,并将该块放到dirty list检查点队列,等待dbwr进行写操作. 二.创建新的undo表空间替换

使用DMV,诊断和调优DB性能。

使用DMV,诊断和调优DB性能. 查看等待统计信息,找出SQL Server慢在哪里: SELECT wait_type , SUM(wait_time_ms / 1000) AS [wait_time_s] FROM sys.dm_os_wait_stats DOWS WHERE wait_type NOT IN ( N'BROKER_EVENTHANDLER', N'BROKER_RECEIVE_WAITFOR', N'BROKER_TASK_STOP', N'BROKER_TO_FLUSH

Java调优

Java调优经验谈 对于调优这个事情来说,一般就是三个过程: 性能监控:问题没有发生,你并不知道你需要调优什么?此时需要一些系统.应用的监控工具来发现问题. 性能分析:问题已经发生,但是你并不知道问题到底出在哪里.此时就需要使用工具.经验对系统.应用进行瓶颈分析,以求定位到问题原因. 性能调优:经过上一步的分析定位到了问题所在,需要对问题进行解决,使用代码.配置等手段进行优化. Java调优也不外乎这三步. 此外,本文所讲的性能分析.调优等是抛开以下因素的: 系统底层环境:硬件.操作系统等 数据

MySQL参数调优最佳实践

前言很多时候,RDS用户经常会问如何调优RDS MySQL的参数,为了回答这个问题,写一篇blog来进行解释: 哪一些参数不能修改,那一些参数可以修改:这些提供修改的参数是不是已经是最佳设置,如何才能利用好这些参数:哪些参数可以改细心的用户在购买RDS的时候都会看到,不同规格能够提供的最大连接数以及内存是不同的,所以这一些产品规格的限制参数:连接数.内存用户是不能够修改的,如果内存或者连接数出现了瓶颈: 内存瓶颈:实例会出现OOM,然后导致主备发生切换连接数瓶颈:应用不能新建立连接到数据库则需要

Eclipse设置、调优、使用

eclipse调优 一般在不对eclipse进行相关设置的时候,使用eclipse总是会觉得启动好慢,用起来好卡,其实只要对eclipse的相关参数进行一些配置,就会有很大的改善. 加快启动速度 1.在eclipse启动的时候,它总是会搜索让其运行的jre,往往就是这个搜索过程让eclipse启动变慢了.(没设置时,等2-3s出现进度条,设置后直接出现进度条) 只要在eclipse.ini中加入-vm的参数就可以了 2.取消所有启动时要激活的插件(在用时激活也一样)和其它的相关的在启动时执行的操

Xpert 调优

-- 10046 event 可以定义 SQL TRACE 级别 /* || 默认的10046级别跟 SQL TRACE 一样, 另外还有一些级别: || level 1: SQL Tracing || level 4: Tracing with bind variable values || level 8: Tracing with wait events || level 12 Tracing with bind variables and wait events (4+8=12) lev

SQL Server 性能调优3 之索引(Index)的维护

SQL Server 性能调优3 之索引(Index)的维护 热度1 评论 16 作者:溪溪水草 前言 前一篇的文章介绍了通过建立索引来提高数据库的查询性能,这其实只是个开始.后续如果缺少适当的维护,你先前建立的索引甚至会成为拖累,成为数据库性能的下降的帮凶. 查找碎片 消除碎片可能是索引维护最常规的任务,微软官方给出的建议是当碎片等级为 5% - 30% 之间时采用 REORGANIZE 来“重整”索引,如果达到 30% 以上则使用 REBUILD 来“重建”索引.决定采用何种手段和操作时机可