关于Go的四种字符串拼接及性能比较'

2020-01-08  本文已影响0人  leejnull

先上代码

// fmt.Sprintf
func BenchmarkStringSprintf(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var str string
        for j := 0; j < numbers; j++ {
            str = fmt.Sprintf("%s%d", str, j)
        }
    }
    b.StopTimer()
}

// add
func BenchmarkStringAdd(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var str string
        for j := 0; j < numbers; j++ {
            str = str + string(j)
        }
    }
    b.StopTimer()
}

// bytes.Buffer
func BenchmarkStringBuffer(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var buffer bytes.Buffer
        for j := 0; j < numbers; j++ {
            buffer.WriteString(strconv.Itoa(j))
        }
        _ = buffer.String()
    }
    b.StopTimer()
}

// strings.Builder
func BenchmarkStringBuilder(b *testing.B) {
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        var builder strings.Builder
        for j := 0; j < numbers; j++ {
            builder.WriteString(strconv.Itoa(j))
        }
        _ = builder.String()
    }
    b.StopTimer()
}

运行结果

lijun:benchmark/ $ go test -bench=.                                                           [10:01:20]
goos: darwin
goarch: amd64
pkg: class12/benchmark
BenchmarkStringSprintf-4              30          47358694 ns/op
BenchmarkStringAdd-4                  50          27664814 ns/op
BenchmarkStringBuffer-4            10000            184422 ns/op
BenchmarkStringBuilder-4           10000            157039 ns/op
PASS
ok      class12/benchmark       6.350s
lijun:benchmark/ $                                                                            [10:01:58]

如果还不知道 Go 的 benchmark,可以先去了解一下,个人认为还是非常不错的性能测试的工具。

得出结论

四种拼接字符串的方式,性能比较结果
strings.Builder > bytes.Buffer > string add > fmt.Sprintf

为什么?

这里我们还是直接看源码吧

先看 Sprintf
// Sprintf formats according to a format specifier and returns the resulting string.
func Sprintf(format string, a ...interface{}) string {
    p := newPrinter()
    p.doPrintf(format, a)
    s := string(p.buf)
    p.free()
    return s
}

这是 fmt.Sprintf 的源码,我们可以看到内部会通过 newPrinter 创建一个新对象 p,点进去看一下 newPrinter 这个函数

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.fmt.init(&p.buf)
    return p
}

它会从系统的临时对象池中那 pp 这个对象,关于临时对象池(sync.Pool),下次有机会再探讨。这里可以知道, Sprintf 会从临时对象池中获取一个 *pp 的指针,然后再做一些格式化的操作,doPrintf 代码就不贴了,格式化后的底层字节会放到 []byte 这个切片里面,最后再 string 转换成字符串返回,并且释放掉 p 对象。
整个过程:创建对象 - 格式化操作 - string化 - 释放对象

接下来看 string

string 是在 Go 里面是一个不可变类型,所以下面的代码

str = str + str2

每次都会创建一个新的 string 类型的值,然后重新赋值给 str 这个变量,相比于上面的 Sprintf 主要少了格式化这个过程,所以在性能上肯定要优于 Sprintf

bytes.Buffer

我们看一下 builder 的 String() 函数源码

// String returns the contents of the unread portion of the buffer
// as a string. If the Buffer is a nil pointer, it returns "<nil>".
//
// To build strings more efficiently, see the strings.Builder type.
func (b *Buffer) String() string {
    if b == nil {
        // Special case, useful in debugging.
        return "<nil>"
    }
    return string(b.buf[b.off:])
}

字符串的底层结构是一个 []byte 的字节序列,而 Buffer 是直接获取未读取的 []byte序列,在转成 string 返回,少了重复创建对象这个步骤。
b.buf 是 []byte 切片
b.off 是已读取的字节位置

strings.Builder
// String returns the accumulated string.
func (b *Builder) String() string {
    return *(*string)(unsafe.Pointer(&b.buf))
}

strings.Builder 直接通过指针来操作了,在效率上更进一步。

总结

通过源码来分析,还是比较清晰明了的,但是限于我自身的水平,对于源码的解读并不都是特别深入,这里也是给大家做出一个参考。关于最后的通过转换成指针来返回字符串的操作,我也就是知道转成指针效率会高,但是关于为什么,也都是模棱两可(是因为直接通过操作内存地址吗)。总之关于基础性、底层的东西还是要多多学习。

来自 https://leejnull.github.io/2019/08/29/2019-08-29/

上一篇下一篇

猜你喜欢

热点阅读