いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-231 (メモリコピー無しで string から []byte へ変換する)(unsafe)

概要

使うことはほぼ無いと思いますが、知っておくと役に立つときが来るかもしれません。

Goで string から []byte へ変換したい場合は、通常以下のように

b := []byte(s)

とします。これで何も問題ないのですが、内部ではメモリコピーが走りますので

極端に大きなサイズの文字列を変換しようとすると時間がかかります。

メモリ上に配置されてるデータは同じで、表現の仕方だけの変更で済めば速くなります。

(C言語でよくやるやつですね)

しかし、Goは暗黙的な型変換を許していません。明示的な変換が必要です。

devlights.hatenablog.com

なので、Goでこのようなことをするには unsafe パッケージを使う必要があります。

unsafeパッケージは、その名前の通り safe ではありません。なので、通常使わないのが正解です。

以下、サンプルです。

サンプル

package zeromemorycopy

import (
    "fmt"
    "io"
    "reflect"
    "strconv"
    "strings"
    "time"
    "unsafe"

    "github.com/devlights/gomy/times"
    "github.com/devlights/gomy/zeromemcpy"
)

// StringToByteSlice -- 文字列からバイトスライスへメモリコピー無しに変換するサンプルです。
//
// REFERENCES
//   - https://stackoverflow.com/questions/59209493/how-to-use-unsafe-get-a-byte-slice-from-a-string-without-memory-copy
//   - https://github.com/devlights/gomy/blob/master/zeromemcpy/s2b.go
//   - https://pkg.go.dev/unsafe@go1.18.4#Slice
func StringToByteSlice() error {
    //
    // string から []byte へメモリコピー無しに変換するには unsafe パッケージを使う必要がある。
    // unsafeパッケージは文字通り unsafe な操作を行うパッケージなので、通常時には利用するべきではない。
    // パフォーマンスが極端に求められている場合で、且つ、メモリコピーの部分がボトルネックな場合にのみ利用するべき。
    // (通常、このような部分よりも他の部分がボトルネックになっているはず)
    //
    // Goは、基本的にこのようなトリッキーなことをしなくても充分速い。
    //

    // -------------------------------------
    // 大きなサイズの文字列を作る
    // -------------------------------------
    var sb strings.Builder
    for i := 0; i < 30_000_000; i++ {
        sb.WriteString(strconv.Itoa(i))
    }

    s := sb.String()
    fmt.Printf("[length] %vbytes, %vmb\n", len(s), len(s)/1024/1024)

    // -------------------------------------
    // 普通に変換
    // -------------------------------------
    elapsed := times.Stopwatch(func(start time.Time) {
        io.Discard.Write([]byte(s))
    })
    fmt.Printf("[normal] %v\n", elapsed)

    // -------------------------------------
    // メモリコピー無しで変換
    // -------------------------------------
    elapsed = times.Stopwatch(func(start time.Time) {
        io.Discard.Write(unsafe.Slice((*byte)(unsafe.Pointer((*reflect.StringHeader)(unsafe.Pointer(&s)).Data)), len(s)))

        /* 上を細かく区切ると以下のようになる
       var (
           ptrStr     = unsafe.Pointer(&s)
           strHeader  = (*reflect.StringHeader)(ptrStr)
           ptrStrData = unsafe.Pointer(strHeader.Data)
           ptrByte    = (*byte)(ptrStrData)
           slice      = unsafe.Slice(ptrByte, len(s))
       )
       io.Discard.Write(slice)
       */
    })
    fmt.Printf("[zeromemcpy] %v\n", elapsed)

    // -------------------------------------
    // zeromemcpy.s2b
    // -------------------------------------
    elapsed = times.Stopwatch(func(start time.Time) {
        io.Discard.Write(zeromemcpy.S2B(s))
    })
    fmt.Printf("[zeromemcpy.s2b] %v\n", elapsed)

    return nil
}

上記のテクニックは、以下の stackoverflow で知りました。

stackoverflow.com

実行すると以下のようになります。

gitpod /workspace/try-golang (master) $ task run
task: [run] go run . -onetime

ENTER EXAMPLE NAME: zeromemorycopy_string_to_byteslice

[Name] "zeromemorycopy_string_to_byteslice"
[length] 228888890bytes, 218mb
[normal] 44.690529ms
[zeromemcpy] 190ns
[zeromemcpy.s2b] 80ns


[Elapsed] 1.404789836s

すごく速くなっているように見えますが、それでも元の状態でも 44ms しかかかっていません。

無理して使う必要はないですね。

参考情報

gomy/s2b.go at master · devlights/gomy · GitHub

gomy/s2b_test.go at master · devlights/gomy · GitHub


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。