Golang string []byte 高性能转换

前言

有时候经常要在golang用到byte与string的转换,然而在golang中通过string()和[]byte()的转换是需要copy的,通过benchmark就可以看出这样的转换存在一定的开销对gc也是不友好的。

1
2
3
4
5
6
func Benchmark_str2bytesByNormal(b *testing.B) {
    testStr := "test"
    for i := 0; i < b.N; i++ {
         _ = []byte(testStr)
    }
}
1
2
3
4
5
goos: windows
goarch: amd64
pkg: playground
Benchmark_str2bytesByNormal-16        161724543             7.38 ns/op
PASS

可以看到上面的benchmark一个op的耗时为7.38ns

查看string源码

golang的string实现在src/runtime/string.go,stringStruct即是string类型的本尊。len根据望文生义,就是string的长度了。这个str其实是个指向具体数据数组的指针。

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

那么要如何探索呢? 这里可以用到unsafe.Pointer然后强制转型为在我们可访问包里的stringStruct,然后通过goland打个断点观察。

可以看到len确实是string对应的长度,由于str是个unsafe.Pointer,所以goland也无法直接查看了。
由于是指向数组的指针,我们可以利用地址偏移的方式进行打印,从而来验证我们的结论。

1
2
3
4
5
6
7
8
func main()  {
    str := "test"
    s := (*stringStruct)(unsafe.Pointer(&str))
    for i := 0; i < s.len; i++ {
        char := *(*byte)(unsafe.Pointer(uintptr(s.str) + uintptr(i)))
        print(string(char))
    }
}

通过运行可以看到,确实输出了预想的内容

slice源码

slice的源码同样在src/runtime下,slice.go即是slice的源码所在之处,文件开头的结构体即是slice的本尊。

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

其实他的array跟stringstruct里的是一模一样的东西,通过类似的代码可以再次验证

通过观察我们可以得出string跟[]byte的结构在内存上其实十分相似,只不过byte slice比string多了一个cap,也就是容量。到这里,我们就可以通过组装的方式来实现string跟[]byte的互转了。
也就是说string转[]byte,我们可以构造一块内存区域,在最后加上cap,这个cap取len的值即可,然后强制转换。
对于[]byte转string使用unsafe.Pointer后直接强制转换即可。
这样省去了对他们持有的数据的拷贝,提高了性能。

具体实现

1
2
3
4
5
6
7
8
9
10
func str2bytes(s string) []byte {
    x := (*[2]uintptr)(unsafe.Pointer(&s))
    h := [3]uintptr{x[0], x[1], x[1]}
    return *(*[]byte)(unsafe.Pointer(&h))
}


func bytes2str(b []byte) string {
    return *(*string)(unsafe.Pointer(&b))
}

性能对比

通过对str2bytes进行benchmark,结果如下。可见每次op的时间显著减小。

1
2
3
4
5
goos: windows
goarch: amd64
pkg: playground
Benchmark_str2bytes-16        1000000000             0.387 ns/op
PASS
作者

ZhongHuihong

发布于

2020-06-17

更新于

2021-10-02

许可协议