Go1.20で追加された「unsafe.StringData、unsafe.String、unsafe.SliceData」による[]byteとstringの変換処理
TL;DR
- Go1.20からメモリ効率を重視した
[]byte
とstring
の変換処理として以下が提供されている- string -> []byte:
unsafe.Slice(unsafe.StringData(s), len(s))
- []byte → string:
unsafe.String(&b[0], len(b))
- string -> []byte:
目次
従来の変換方法
Goにおいて[]byte
とstring
で相互に変換を行う際、簡潔な方法として以下のように型キャストする実装方法があります。
// string -> []byte []byte(s) // []byte → string string(b)
しかし[]byte
とstring
は内部のデータ構造が異なるため、上記の方法では無駄なメモリアロケーションが発生し、パフォーマンス面に難があります。
その問題に対処するため、公式のドキュメントなどで明示されていないですが、以下のような手法がこれまで広く使われていました。
// string -> []byte *(*[]byte)(unsafe.Pointer(&s)) // []byte → string *(*string)(unsafe.Pointer(&b))
但し上記の方法は、本来想定されていない無理矢理な変換方法なので、使用方法を誤ればバグを生み出すリスクがありました。
新しい変換方法
この問題に対処するため、Go1.20にて「unsafe.StringData、unsafe.String、unsafe.SliceData」が追加されたことで、以下の方法が推奨されるようになりました。
// string -> []byte unsafe.Slice(unsafe.StringData(s), len(s)) // []byte → string unsafe.String(&b[0], len(b))
ベンチマーク
import ( "testing" "unsafe" ) func BenchmarkTest1(b *testing.B) { for i := 0; i < b.N; i++ { test1() } } func BenchmarkTest2(b *testing.B) { for i := 0; i < b.N; i++ { test2() } } func BenchmarkTest3(b *testing.B) { for i := 0; i < b.N; i++ { test3() } } func test1() { origin := "test" // string -> []byte b := []byte(origin) // []byte → string _ = string(b) } func test2() { origin := "test" // string -> []byte b := *(*[]byte)(unsafe.Pointer(&origin)) // []byte → string _ = *(*string)(unsafe.Pointer(&b)) } func test3() { origin := "test" // string -> []byte b := unsafe.Slice(unsafe.StringData(origin), len(origin)) // []byte → string _ = unsafe.String(&b[0], len(b)) }
$ go test -bench . -benchmem goos: darwin goarch: arm64 BenchmarkTest1-10 234380760 4.933 ns/op 0 B/op 0 allocs/op BenchmarkTest2-10 1000000000 0.2888 ns/op 0 B/op 0 allocs/op BenchmarkTest3-10 1000000000 0.4360 ns/op 0 B/op 0 allocs/op PASS ok command-line-arguments 2.790s
単純な型キャストと比べると、新しい方法での変換も非常にパフォーマンスが良いことが分かります。
活用事例
既にGinやTinyGoなどのライブラリでは取り入れられているようで、今後どんどん置換が進んでいきそうです。