地方エンジニアの学習日記

興味ある技術の雑なメモだったりを書いてくブログ。たまに日記とガジェット紹介。

【Go】[]byteを文字列として扱うならstring([]byte)よりもunsafe.Pointer()を使ったほうが早い

まとめ

var tmp []byte

// こうするより
str := string([]byte)

// こうした方がメモリコピーが発生しないので早い
str := *(*string)(unsafe.Pointer(&tmp))

本文

以下のような関数をベンチマークしたいときに使える話です。

package main

import (
    "os"
    "reflect"
    "testing"
    "unsafe"
)

func pre() []byte {
    filename := "./file.txt"
    f, _ := os.Open(filename)
    m := 948024
    p := make([]byte, m)
    f.Read(p)
    return p
}

前処理はこんな感じでテストしたい関数が以下のようにあるとする

func Benchmark_String(b *testing.B) {
    p := pre()
    c := "aaa"
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        a := string(p)
        if a == c {
            continue
        }
    }
}

この関数と以下の関数ではパフォーマンスにめちゃめちゃ差がある。

func Benchmark_Unsafe(b *testing.B) {
    p := pre()
    c := "aaa"
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        a := *(*string)(unsafe.Pointer(&p))
        if a == c {
            continue
        }
    }
}

go testを使ってベンチマークをして見ると凄まじい差が出ている。ファイルのサイズは100kb程度なので1ループごとに毎回メモリアロケーションが走っていることがわかります。*(*string)(unsafe.Pointer(&p))ではunsafe.PointerというCでいう(void *)にキャストして(*string)型にキャストしてデリファレンスしてstringとして扱えるようにしています。(Go の型管理の機構をすっ飛ばしてプログラマが任意のアドレスを読み書きする際に使える機能でこの時点で危険な匂いがとてもしますw)

Benchmark_String-4            100        161550 ns/op      950276 B/op          1 allocs/op
Benchmark_Unsafe-4           100             2.910 ns/op           0 B/op          0 allocs/op

何が違うのか

Benchmark_Stringの方のアセンブラを出力してみるとたしかに処理の途中でメモリのコピーが行われているのがわかりました。[]byteとstringでそれぞれ確保する必要があるようです。

"".Benchmark_String STEXT size=135 args=0x8 locals=0x58 funcid=0x0 align=0x0
    0x001e 00030 (./main_test.go:25)    MOVQ AX, "".p.ptr+72(SP)
    0x0023 00035 (./main_test.go:25)    MOVQ BX, "".p.len+24(SP)
    0x0028 00040 (./main_test.go:23)    MOVQ "".b+96(SP), AX
    0x002d 00045 (./main_test.go:23)    PCDATA   $1, $1
    0x002d 00045 (./main_test.go:23)    CALL testing.(*B).ResetTimer(SB)
    0x0032 00050 (./main_test.go:23)    XORL AX, AX
    0x0034 00052 (./main_test.go:24)    JMP  88
    0x0036 00054 (./main_test.go:24)    MOVQ AX, "".i+32(SP)
    0x003b 00059 (./main_test.go:25)    LEAQ ""..autotmp_3+40(SP), AX
    0x0040 00064 (./main_test.go:25)    MOVQ "".p.ptr+72(SP), BX
    0x0045 00069 (./main_test.go:25)    MOVQ "".p.len+24(SP), CX
    0x004a 00074 (./main_test.go:25)    CALL runtime.slicebytetostring(SB) // ここでメモリコピーをしている
    0x004f 00079 (./main_test.go:24)    MOVQ "".i+32(SP), DX
    0x0054 00084 (./main_test.go:24)    LEAQ 1(DX), AX
    0x0058 00088 (./main_test.go:24)    MOVQ "".b+96(SP), DX
    0x005d 00093 (./main_test.go:24)    NOP
    0x0060 00096 (./main_test.go:24)    CMPQ 400(DX), AX
    0x0067 00103 (./main_test.go:24)    JGT  54
    0x0069 00105 (./main_test.go:28)    PCDATA   $1, $-1
    0x0069 00105 (./main_test.go:28)    MOVQ 80(SP), BP

Benchmark_Unsafeでは以下のようになっておりメモリコピーが起きていないのがわかります。[]byteの値をstring型の変数へコピーするのではなくstring型として読むという感じです。

"".Benchmark_Unsafe STEXT size=186 args=0x8 locals=0x40 funcid=0x0 align=0x0
    0x0000 00000 (./main_test.go:41)    TEXT "".Benchmark_Unsafe(SB), ABIInternal, $64-8
    0x0000 00000 (./main_test.go:41)    CMPQ SP, 16(R14)
    0x0004 00004 (./main_test.go:41)    PCDATA   $0, $-2
    0x0004 00004 (./main_test.go:41)    JLS  166
    0x000a 00010 (./main_test.go:41)    PCDATA   $0, $-1
    0x000a 00010 (./main_test.go:41)    SUBQ $64, SP
    0x000e 00014 (./main_test.go:41)    MOVQ BP, 56(SP)
    0x0013 00019 (./main_test.go:41)    LEAQ 56(SP), BP
    0x0018 00024 (./main_test.go:41)    FUNCDATA $0, gclocals·c7c4fc7b12f6707ea74acf7400192967(SB)
    0x0018 00024 (./main_test.go:41)    FUNCDATA $1, gclocals·47a67f4fb109a79e4380e4f8459439e0(SB)
    0x0018 00024 (./main_test.go:41)    FUNCDATA $2, "".Benchmark_Unsafe.stkobj(SB)
    0x0018 00024 (./main_test.go:41)    FUNCDATA $5, "".Benchmark_Unsafe.arginfo1(SB)
    0x0018 00024 (./main_test.go:41)    FUNCDATA $6, "".Benchmark_Unsafe.argliveinfo(SB)
    0x0018 00024 (./main_test.go:41)    PCDATA   $3, $1
    0x0018 00024 (./main_test.go:41)    MOVQ AX, "".b+72(SP)
    0x001d 00029 (./main_test.go:41)    PCDATA   $3, $-1
    0x001d 00029 (./main_test.go:42)    PCDATA   $1, $0
    0x001d 00029 (./main_test.go:42)    NOP
    0x0020 00032 (./main_test.go:42)    CALL "".pre(SB)
    0x0025 00037 (./main_test.go:42)    MOVQ AX, "".p+32(SP)
    0x002a 00042 (./main_test.go:42)    MOVQ BX, "".p+40(SP)
    0x002f 00047 (./main_test.go:42)    MOVQ CX, "".p+48(SP)
    0x0034 00052 (./main_test.go:44)    MOVQ "".b+72(SP), AX
    0x0039 00057 (./main_test.go:44)    PCDATA   $1, $1
    0x0039 00057 (./main_test.go:44)    CALL testing.(*B).ResetTimer(SB)
    0x003e 00062 (./main_test.go:45)    MOVQ "".b+72(SP), AX
    0x0043 00067 (./main_test.go:45)    XORL CX, CX
    0x0045 00069 (./main_test.go:45)    JMP  74
    0x0047 00071 (./main_test.go:45)    INCQ CX
    0x004a 00074 (./main_test.go:45)    CMPQ 400(AX), CX
    0x0051 00081 (./main_test.go:45)    JLE  156
    0x0053 00083 (./main_test.go:46)    MOVQ "".p+40(SP), DX
    0x0058 00088 (./main_test.go:46)    MOVQ "".p+32(SP), SI
    0x005d 00093 (./main_test.go:46)    NOP
    0x0060 00096 (./main_test.go:47)    CMPQ DX, $3
    0x0064 00100 (./main_test.go:47)    JNE  71
    0x0066 00102 (./main_test.go:45)    MOVQ CX, "".i+24(SP)
    0x006b 00107 (./main_test.go:47)    MOVQ SI, AX
    0x006e 00110 (./main_test.go:47)    LEAQ go.string."aaa"(SB), BX
    0x0075 00117 (./main_test.go:47)    MOVQ DX, CX
    0x0078 00120 (./main_test.go:47)    CALL runtime.memequal(SB)
    0x007d 00125 (./main_test.go:47)    NOP
    0x0080 00128 (./main_test.go:47)    TESTB    AL, AL
    0x0082 00130 (./main_test.go:47)    JEQ  144
    0x0084 00132 (./main_test.go:45)    MOVQ "".b+72(SP), AX
    0x0089 00137 (./main_test.go:45)    MOVQ "".i+24(SP), CX
    0x008e 00142 (./main_test.go:48)    JMP  71
    0x0090 00144 (./main_test.go:45)    MOVQ "".b+72(SP), AX
    0x0095 00149 (./main_test.go:45)    MOVQ "".i+24(SP), CX
    0x009a 00154 (./main_test.go:47)    JMP  71
    0x009c 00156 (./main_test.go:51)    PCDATA   $1, $-1
    0x009c 00156 (./main_test.go:51)    MOVQ 56(SP), BP
    0x00a1 00161 (./main_test.go:51)    ADDQ $64, SP
    0x00a5 00165 (./main_test.go:51)    RET
    0x00a6 00166 (./main_test.go:51)    NOP
    0x00a6 00166 (./main_test.go:41)    PCDATA   $1, $-1
    0x00a6 00166 (./main_test.go:41)    PCDATA   $0, $-2
    0x00a6 00166 (./main_test.go:41)    MOVQ AX, 8(SP)
    0x00ab 00171 (./main_test.go:41)    CALL runtime.morestack_noctxt(SB)
    0x00b0 00176 (./main_test.go:41)    MOVQ 8(SP), AX
    0x00b5 00181 (./main_test.go:41)    PCDATA   $0, $-1
    0x00b5 00181 (./main_test.go:41)    JMP  0

メモリコピーをしている処理はこの辺らしいです。memmoveの先はアセンブリで記述されています。

github.com

おまけ

キャストした結果をどこかへ格納しないようにして直接比較するとコンパイラが最適化してくれるのかunsafe.Pointerのときと同じような処理時間になっていました。

func Benchmark_String2(b *testing.B) {
        p := pre()
        c := "aaa"
        b.ResetTimer()
        for i := 0; i < b.N; i++ {
                if string(p) == c {
                        continue
                }
        }
}

アセンブラ

"".Benchmark_String2 STEXT size=179 args=0x8 locals=0x38 funcid=0x0 align=0x0
    0x0000 00000 (./main_test.go:80)    TEXT "".Benchmark_String2(SB), ABIInternal, $56-8
    0x0000 00000 (./main_test.go:80)    CMPQ SP, 16(R14)
    0x0004 00004 (./main_test.go:80)    PCDATA   $0, $-2
    0x0004 00004 (./main_test.go:80)    JLS  159
    0x000a 00010 (./main_test.go:80)    PCDATA   $0, $-1
    0x000a 00010 (./main_test.go:80)    SUBQ $56, SP
    0x000e 00014 (./main_test.go:80)    MOVQ BP, 48(SP)
    0x0013 00019 (./main_test.go:80)    LEAQ 48(SP), BP
    0x0018 00024 (./main_test.go:80)    FUNCDATA $0, gclocals·c7c4fc7b12f6707ea74acf7400192967(SB)
    0x0018 00024 (./main_test.go:80)    FUNCDATA $1, gclocals·663f8c6bfa83aa777198789ce63d9ab4(SB)
    0x0018 00024 (./main_test.go:80)    FUNCDATA $5, "".Benchmark_String2.arginfo1(SB)
    0x0018 00024 (./main_test.go:80)    FUNCDATA $6, "".Benchmark_String2.argliveinfo(SB)
    0x0018 00024 (./main_test.go:80)    PCDATA   $3, $1
    0x0018 00024 (./main_test.go:80)    MOVQ AX, "".b+64(SP)
    0x001d 00029 (./main_test.go:80)    PCDATA   $3, $-1
    0x001d 00029 (./main_test.go:81)    PCDATA   $1, $0
    0x001d 00029 (./main_test.go:81)    NOP
    0x0020 00032 (./main_test.go:81)    CALL "".pre(SB)
    0x0025 00037 (./main_test.go:85)    MOVQ AX, "".p.ptr+40(SP)
    0x002a 00042 (./main_test.go:85)    MOVQ BX, "".p.len+24(SP)
    0x002f 00047 (./main_test.go:83)    MOVQ "".b+64(SP), AX
    0x0034 00052 (./main_test.go:83)    PCDATA   $1, $1
    0x0034 00052 (./main_test.go:83)    CALL testing.(*B).ResetTimer(SB)
    0x0039 00057 (./main_test.go:84)    MOVQ "".b+64(SP), AX
    0x003e 00062 (./main_test.go:84)    MOVQ "".p.len+24(SP), CX
    0x0043 00067 (./main_test.go:84)    XORL DX, DX
    0x0045 00069 (./main_test.go:84)    JMP  74
    0x0047 00071 (./main_test.go:84)    INCQ DX
    0x004a 00074 (./main_test.go:84)    CMPQ 400(AX), DX
    0x0051 00081 (./main_test.go:84)    JLE  149
    0x0053 00083 (./main_test.go:85)    CMPQ CX, $3
    0x0057 00087 (./main_test.go:85)    JNE  71
    0x0059 00089 (./main_test.go:84)    MOVQ DX, "".i+32(SP)
    0x005e 00094 (./main_test.go:85)    MOVQ "".p.ptr+40(SP), AX
    0x0063 00099 (./main_test.go:85)    LEAQ go.string."aaa"(SB), BX
    0x006a 00106 (./main_test.go:85)    CALL runtime.memequal(SB)
    0x006f 00111 (./main_test.go:85)    TESTB    AL, AL
    0x0071 00113 (./main_test.go:85)    JEQ  132
    0x0073 00115 (./main_test.go:84)    MOVQ "".b+64(SP), AX
    0x0078 00120 (./main_test.go:85)    MOVQ "".p.len+24(SP), CX
    0x007d 00125 (./main_test.go:84)    MOVQ "".i+32(SP), DX
    0x0082 00130 (./main_test.go:86)    JMP  71
    0x0084 00132 (./main_test.go:84)    MOVQ "".b+64(SP), AX
    0x0089 00137 (./main_test.go:85)    MOVQ "".p.len+24(SP), CX
    0x008e 00142 (./main_test.go:84)    MOVQ "".i+32(SP), DX
    0x0093 00147 (./main_test.go:85)    JMP  71
    0x0095 00149 (./main_test.go:89)    PCDATA   $1, $-1
    0x0095 00149 (./main_test.go:89)    MOVQ 48(SP), BP
    0x009a 00154 (./main_test.go:89)    ADDQ $56, SP
    0x009e 00158 (./main_test.go:89)    RET
    0x009f 00159 (./main_test.go:89)    NOP
    0x009f 00159 (./main_test.go:80)    PCDATA   $1, $-1
    0x009f 00159 (./main_test.go:80)    PCDATA   $0, $-2
    0x009f 00159 (./main_test.go:80)    MOVQ AX, 8(SP)
    0x00a4 00164 (./main_test.go:80)    CALL runtime.morestack_noctxt(SB)
    0x00a9 00169 (./main_test.go:80)    MOVQ 8(SP), AX
    0x00ae 00174 (./main_test.go:80)    PCDATA   $0, $-1
    0x00ae 00174 (./main_test.go:80)    JMP  0

追記

よりよいやり方があるらしい。(手元のgoバージョンは古かったので未検証ですがunsafe.StringData("Hello")だけで済みそうなのはよさそうです)

mattn.kaoriya.net