25

Go语言中[]byte和string类型相互转换时的性能分析和优化 | yoko blog

 4 years ago
source link: https://pengrl.com/p/31544/?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Go语言中[]byte和string类型相互转换时的性能分析和优化

2019-07-01 | Go

我们在使用Go语言时,经常涉及到[]byte和string两种类型间的转换。本篇文章将讨论转换时的开销,Go编译器在一些特定场景下对转换做的优化,以及在高性能场景下,我们自己如何做相应的优化。

[]byte其实就是byte类型的切片,对应的底层结构体定义如下(在runtime/slice.go文件中)

1
2
3
4
5
type slice struct {
array unsafe.Pointer
len int
cap int
}

string对应的底层结构体定义如下(在runtime/string.go文件中)

1
2
3
4
type stringStruct struct {
str unsafe.Pointer
len int
}

可以看到它们内部都有一个指针类型(array或str),指向真实数据。另外还有一个len字段,标识数据的长度。
slice多了一个cap字段,表示容量大小。当要往slice尾部追加数据而空余容量又不够时,会重新分配更大的内存块,将当前内存块的内容拷贝至新内存块,再在新内存块做追加。

slice变量间做赋值操作时,只是修改指针指向,不会拷贝真实数据。string变量间赋值也是同样的道理。

但是[]byte和string相互转换,就需要重新申请内存并拷贝内存了。因为Go语义中,slice的内容是可变的(mutable),而string是不可变的(immutable)。如果他们底部指向同一块数据,那么由于slice可对数据做修改,string就做不到immutable了。

[]byte和string互转时的底层调用分别对应runtime/string.gostringtoslicebyteslicebytetostring两个函数。

那么如果我们想省去申请和拷贝内存的开销呢?
来看runtime/string.goslicebytetostringtmpstringtoslicebytetmp两个函数,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func slicebytetostringtmp(b []byte) string {
// Return a "string" referring to the actual []byte bytes.
// This is only for use by internal compiler optimizations
// that know that the string form will be discarded before
// the calling goroutine could possibly modify the original
// slice or synchronize with another goroutine.
// First such case is a m[string(k)] lookup where
// m is a string-keyed map and k is a []byte.
// Second such case is "<"+string(b)+">" concatenation where b is []byte.
// Third such case is string(b)=="foo" comparison where b is []byte.

if raceenabled && len(b) > 0 {
racereadrangepc(unsafe.Pointer(&b[0]),
uintptr(len(b)),
getcallerpc(unsafe.Pointer(&b)),
funcPC(slicebytetostringtmp))
}
return *(*string)(unsafe.Pointer(&b))
}

func stringtoslicebytetmp(s string) []byte {
// Return a slice referring to the actual string bytes.
// This is only for use by internal compiler optimizations
// that know that the slice won't be mutated.
// The only such case today is:
// for i, c := range []byte(str)

str := (*stringStruct)(unsafe.Pointer(&s))
ret := slice{array: unsafe.Pointer(str.str), len: str.len, cap: str.len}
return *(*[]byte)(unsafe.Pointer(&ret))
}

通过unsafe.Pointer直接做指针类型的转换。

注释中也说得很清楚了。

stringtoslicebytetmp调用的前提是保证返回的[]byte之后不会被修改,只用于编译器内部优化,目前唯一的场景是在for loop中将string转换成[]byte做遍历操作时,比如 for i, c := range []byte(str)

slicebytetostringtmp调用的前提其实也是类似,保证返回的string在生命周期结束之前,[]byte不会被修改,也是只用于编译器内部优化,目前有三种场景:

  1. 假设有一个key为string的map遍历m,你想使用[]byte类型的变量k做查找操作,比如 m[string(k)]
  2. 做字符串拼接操作时,比如 <"+string(b)+">,其中b是[]byte类型
  3. []byte类型和常量字符串做比较操作,比如 string(b)=="foo"

由于以上两个函数是不暴露给Go用户的,所以如果我们在一些高性能场景想要做类似优化时,可以通过unsafe.Pointer自己做类似实现,当然,前提是保证数据是immutable的。

参考链接:

本文完,作者yoko,尊重劳动人民成果,转载请注明原文出处: https://pengrl.com/p/31544/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK