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拼接效率分析的主要内容,如果未能解决你的问题,请参考以下文章