まとめ
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の先はアセンブリで記述されています。
おまけ
キャストした結果をどこかへ格納しないようにして直接比較するとコンパイラが最適化してくれるのか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")
だけで済みそうなのはよさそうです)