String拼接效率分析

Posted cherrytab

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了String拼接效率分析相关的知识,希望对你有一定的参考价值。

先po一个基准测试结果

package main

import (
    "bytes"
    "fmt"
    "strings"
    "testing"
)

const v = "Measure the elapsed time between sending a data octet with a?"

func BenchmarkStringJoin(b *testing.B) {
    var s string
    for i := 0; i < b.N; i++ {
        s = strings.Join([]string{s, "[", v, "]"}, "")
    }
}

func BenchmarkStringAdd(b *testing.B) {
    var s string
    for i := 0; i < b.N; i++ {
        s = s + "[" + v + "]"
    }
}

func BenchmarkSprintf(b *testing.B) {
    var s string
    for i := 0; i < b.N; i++ {
        s = fmt.Sprintf("%s[%s]", s, v)
    }
}

func BenchmarkBuffer(b *testing.B) {
    var buf bytes.Buffer
    for i := 0; i < b.N; i++ {
        buf.WriteString("[")
        buf.WriteString(v)
        buf.WriteString("]")
    }
}

 

BenchmarkStringJoin-8              18505            120331 ns/op
BenchmarkStringAdd-8               25566            129192 ns/op
BenchmarkSprintf-8                 12670            126964 ns/op
BenchmarkBuffer-8               15034540               125 ns/op

可以看到bytes.Buffer明显效率高于其他,下面简单分析一下。

 

其实主要是String和byte[]的区别

这里可以去看Rob Pike的一篇相关blog

https://blog.golang.org/strings

 

String

type string

string is the set of all strings of 8-bit bytes, conventionally but not necessarily representing UTF-8-encoded text. A string may be empty, but not nil. Values of string type are immutable.

 

type stringStruct struct {
    str unsafe.Pointer
    len int
}

可以看到其实就是一个指向底层数组的指针,该数组的长度是len。

 

func gostringnocopy(str *byte) string {
    ss := stringStruct{str: unsafe.Pointer(str), len: findnull(str)}
    s := *(*string)(unsafe.Pointer(&ss))
    return s
}

这就是新建字符串的时候,如果我们看string拼接的汇编,就会发现这个函数调用。

 

[]byte

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

看起来很像,但是还是有区别的,可以去看一些这块的底层分析

https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-array-and-slice/

 

区别最常讲的就是string不可变,其实是因为这里的string本身是一个stringStruct{str: str_point, len: str_len}

我们不能在地址上修改,但是可以换一个地址,这样也就会给gc增加任务和多分配一次内存。

s := "A1" // 分配存储"A1"的内存空间,s结构体里的str指针指向这快内存
s = "A2"  // 重新给"A2"的分配内存空间,s结构体里的str指针指向这快内存

 

s := []byte{1} // 分配存储1数组的内存空间,s结构体的array指针指向这个数组。
s = []byte{2}  // 将array的内容改为2

 

转换

只要牵扯转换,其实都会有内存的“浪费”,不难理解,因为string操作本身不可避免这个问题,上面分析了。

 

string->[]byte

func stringtoslicebyte(buf *tmpBuf, s string) []byte {
    var b []byte
    if buf != nil && len(s) <= len(buf) {
        *buf = tmpBuf{}
        b = buf[:len(s)]
    } else {
        b = rawbyteslice(len(s))
    }
    copy(b, s)
    return b
}

可以看到b是新分配的,然后再将s复制给b。其中这个copy()也是一个slicestringcopy()实现

func slicestringcopy(to []byte, fm string) int {
    if len(fm) == 0 || len(to) == 0 {
        return 0
    }

    n := len(fm)
    if len(to) < n {
        n = len(to)
    }

    if raceenabled {
        callerpc := getcallerpc()
        pc := funcPC(slicestringcopy)
        racewriterangepc(unsafe.Pointer(&to[0]), uintptr(n), callerpc, pc)
    }
    if msanenabled {
        msanwrite(unsafe.Pointer(&to[0]), uintptr(n))
    }

    memmove(unsafe.Pointer(&to[0]), stringStructOf(&fm).str, uintptr(n))
    return n
}

可以看出没有复用内存。

 

[]byte->string

func slicebytetostring(buf *tmpBuf, b []byte) string {
    l := len(b)
    if l == 0 {
        // Turns out to be a relatively common case.
        // Consider that you want to parse out data between parens in "foo()bar",
        // you find the indices and convert the subslice to string.
        return ""
    }
    if raceenabled && l > 0 {
        racereadrangepc(unsafe.Pointer(&b[0]),
            uintptr(l),
            getcallerpc(unsafe.Pointer(&buf)),
            funcPC(slicebytetostring))
    }
    if msanenabled && l > 0 {
        msanread(unsafe.Pointer(&b[0]), uintptr(l))
    }
    s, c := rawstringtmp(buf, l)
    copy(c, b)
    return s
}

一样的思路

 

转换都没有复用内存,其实还是有些消耗的。

 

boya列出了一个复用的思路

func stringtoslicebyte(s string) []byte {
    sh := (*reflect.StringHeader)(unsafe.Pointer(&s))
    bh := reflect.SliceHeader{
        Data:sh.Data,
        Len:sh.Len,
        Cap:sh.Len,
    }
    return *(*[]byte)(unsafe.Pointer(&bh))
}

func slicebytetostring(b []byte) string {
    bh := (*reflect.SliceHeader)(unsafe.Pointer(&b))
    sh := reflect.StringHeader{
        Data: bh.Data,
        Len:  bh.Len,
    }
    return *(*string)(unsafe.Pointer(&sh))
}

这种就过于“自由”了。

 

参考

https://zboya.github.io/post/golang_byte_slice_and_string/

以上是关于String拼接效率分析的主要内容,如果未能解决你的问题,请参考以下文章

[JAVA] String 拼接效率

String拼接字符串效率低,你知道原因吗?

StringBuffer的拼接效率小细节

测试一下StringBuffer和StringBuilder及字面常量拼接三种字符串的效率

String拼接出现null?你看到的分析可是错的

字符串拼接效率