Go1.20で追加された「unsafe.StringData、unsafe.String、unsafe.SliceData」による[]byteとstringの変換処理

TL;DR

  • Go1.20からメモリ効率を重視した[]bytestringの変換処理として以下が提供されている
    • string -> []byte:unsafe.Slice(unsafe.StringData(s), len(s))
    • []byte → string:unsafe.String(&b[0], len(b))

目次

従来の変換方法

Goにおいて[]bytestringで相互に変換を行う際、簡潔な方法として以下のように型キャストする実装方法があります。

// string -> []byte
[]byte(s)

// []byte → string
string(b)

しかし[]bytestringは内部のデータ構造が異なるため、上記の方法では無駄なメモリアロケーションが発生し、パフォーマンス面に難があります。

その問題に対処するため、公式のドキュメントなどで明示されていないですが、以下のような手法がこれまで広く使われていました。

// 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などのライブラリでは取り入れられているようで、今後どんどん置換が進んでいきそうです。

github.com

github.com

参考リンク

github.com