いろいろ備忘録日記

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

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

概要

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

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

s := string(buf)

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

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

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

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

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

devlights.hatenablog.com

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

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

以下、サンプルです。

サンプル

package zeromemorycopy

import (
    "bytes"
    "fmt"
    "io"
    "strconv"
    "time"
    "unsafe"

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

// ByteSliceToString -- バイトスライスから文字列へメモリコピー無しに変換するサンプルです。
//
// REFERENCES
//   - https://github.com/devlights/gomy/blob/master/zeromemcpy/b2s.go
//   - https://cs.opensource.google/go/go/+/refs/tags/go1.18.4:src/strings/builder.go;l=47
func ByteSliceToString() error {
    //
    // []byte から string へメモリコピー無しに変換するには unsafe パッケージを使う必要がある。
    // unsafeパッケージは文字通り unsafe な操作を行うパッケージなので、通常時には利用するべきではない。
    // パフォーマンスが極端に求められている場合で、且つ、メモリコピーの部分がボトルネックな場合にのみ利用するべき。
    // (通常、このような部分よりも他の部分がボトルネックになっているはず)
    //
    // Goは、基本的にこのようなトリッキーなことをしなくても充分速い。
    //

    // -------------------------------------
    // 大きなサイズのバッファを作る
    // -------------------------------------

    buf := new(bytes.Buffer)
    for i := 0; i < 30_000_000; i++ {
        buf.WriteString(strconv.Itoa(i))
    }

    b := buf.Bytes()
    fmt.Printf("[length] %vbytes, %vmb\n", len(b), len(b)/1024/1024)

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

    // -------------------------------------
    // メモリコピー無しで変換
    // -------------------------------------
    elapsed = times.Stopwatch(func(start time.Time) {
        io.WriteString(io.Discard, *(*string)(unsafe.Pointer(&b)))

        /* 上を細かく区切ると以下のようになる
       var (
           ptrByte = unsafe.Pointer(&b)
           ptrStr  = (*string)(ptrByte)
           str     = *ptrStr
       )
       io.WriteString(io.Discard, str)
       */
    })
    fmt.Printf("[zeromemcpy] %v\n", elapsed)

    // -------------------------------------
    // zeromemcpy.b2s
    // -------------------------------------
    elapsed = times.Stopwatch(func(start time.Time) {
        io.WriteString(io.Discard, zeromemcpy.B2S(b))
    })
    fmt.Printf("[zeromemcpy.B2S] %v\n", elapsed)

    return nil
}

上記のテクニックは、標準ライブラリの strings.Builder.String() の中で行われてます。

https://cs.opensource.google/go/go/+/refs/tags/go1.18.4:src/strings/builder.go;l=47

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

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

ENTER EXAMPLE NAME: zeromemorycopy_byteslice_to_string

[Name] "zeromemorycopy_byteslice_to_string"
[length] 228888890bytes, 218mb
[normal] 43.962061ms
[zeromemcpy] 290ns
[zeromemcpy.B2S] 70ns


[Elapsed] 1.262644644s

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

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

参考情報

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

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


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

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