#問題提起
Goでは&とnewの初期化に関して、一行で初期化できるかどうか以外の違いは無いという文面をよく見ますが本当にそうでしょうか?
#考察
##goプログラムによる検証
package main
import "log"
type hello map[int][]int32
func main() {
var key []int32
hoge := &hello{}
log.Println(hoge)
fuga := new(hello)
log.Println(fuga)
(*hoge)[0] = key
log.Println(hoge)
(*fuga)[0] = key
log.Println(fuga)
}
上記コードの挙動を想像してください。もし同じなら以下のように出力されるはずです
yyyy/mm/dd H:i:s &map[]
yyyy/mm/dd H:i:s &map[]
yyyy/mm/dd H:i:s &map[0:[]]
yyyy/mm/dd H:i:s &map[0:[]]
実際には
yyyy/mm/dd H:i:s &map[]
yyyy/mm/dd H:i:s &map[]
yyyy/mm/dd H:i:s &map[0:[]]
panic: assignment to entry in nil map
と、パニックが起きてしまいます。
一つずつ挙動を追ってみましょう。hoge
、fuga
それぞれ宣言の後に以下の2行を追加してみます。
//上記省略
hoge := &hello{}
log.Println(hoge)
log.Println(hoge == nil) //false
log.Println(*hoge == nil) //false
fuga := new(hello)
log.Println(fuga)
log.Println(fuga == nil) //false
log.Println(*fuga == nil) //true
//以下省略
結果は横にコメントで表示してる通りです。
newで宣言された方でアドレスの先はnil
になっています。
goプログラムだけで挙動を追うのは不可能では無いですが流石に無理があるのでアセンブリを出力してみます。
##出力されたアセンブリによる検証
$ go tool compile -S -S hoge.go
結果は一部の必要な部分のみ表示します。
0x0021 00033 (hoge.go:10) PCDATA $2, $1
0x0021 00033 (hoge.go:10) PCDATA $0, $0
0x0021 00033 (hoge.go:10) LEAQ type."".hello(SB), AX
0x0028 00040 (hoge.go:10) PCDATA $2, $0
0x0028 00040 (hoge.go:10) MOVQ AX, (SP)
0x002c 00044 (hoge.go:10) CALL runtime.newobject(SB)
0x0031 00049 (hoge.go:10) PCDATA $2, $1
0x0031 00049 (hoge.go:10) MOVQ 8(SP), AX
0x0036 00054 (hoge.go:10) PCDATA $2, $0
0x0036 00054 (hoge.go:10) PCDATA $0, $1
0x0036 00054 (hoge.go:10) MOVQ AX, "".hoge+32(SP)
0x003b 00059 (hoge.go:10) CALL runtime.makemap_small(SB)
0x0040 00064 (hoge.go:10) PCDATA $2, $1
0x0040 00064 (hoge.go:10) MOVQ (SP), AX
0x0044 00068 (hoge.go:10) PCDATA $2, $-2
0x0044 00068 (hoge.go:10) PCDATA $0, $-2
0x0044 00068 (hoge.go:10) CMPL runtime.writeBarrier(SB), $0
0x004b 00075 (hoge.go:10) JNE 520
0x0051 00081 (hoge.go:10) MOVQ "".hoge+32(SP), CX
0x0056 00086 (hoge.go:10) MOVQ AX, (CX)
0x0092 00146 (hoge.go:13) PCDATA $2, $1
0x0092 00146 (hoge.go:13) LEAQ type."".hello(SB), AX
0x0099 00153 (hoge.go:13) PCDATA $2, $0
0x0099 00153 (hoge.go:13) MOVQ AX, (SP)
0x009d 00157 (hoge.go:13) CALL runtime.newobject(SB)
0x00a2 00162 (hoge.go:13) PCDATA $2, $1
0x00a2 00162 (hoge.go:13) MOVQ 8(SP), AX
0x00a7 00167 (hoge.go:13) PCDATA $0, $3
0x00a7 00167 (hoge.go:13) MOVQ AX, "".fuga+40(SP)
10行目は
hoge := &hello{}
13行目は
fuga := new(hello)
のコードです。
見てわかる通り全然違います。まぁ挙動が全く同じだったらnewと&の2つもいらないですからね。
では順に違う箇所を見ていきましょう。
まず前半(複合リテラルでのmakemap以前)はこのようになっています。
PCDATA命令や、最後のMOV命令の第二オペランド以外は一緒です。ここまではほぼおなじ挙動と言えます。
気になるところは、どちらもruntime.newobject
を呼び出しています。
このコードを追っていくとmallocgc
という関数が返されています。
よって、newobject
では、メモリ領域を確保し、確保したアドレスの番地を返していることが分かりました。
また、以下のコマンドを実行してみた結果より
$ go build -gcflags -m hoge.go
# command-line-arguments
./hoge.go:11:16: hoge escapes to heap
./hoge.go:10:13: &hello literal escapes to heap
./hoge.go:10:19: &hello literal escapes to heap
./hoge.go:14:16: fuga escapes to heap
./hoge.go:13:16: new(hello) escapes to heap
./hoge.go:17:16: hoge escapes to heap
./hoge.go:20:16: fuga escapes to heap
./hoge.go:11:16: main ... argument does not escape
./hoge.go:14:16: main ... argument does not escape
./hoge.go:17:16: main ... argument does not escape
./hoge.go:20:16: main ... argument does not escape
確保されるメモリ領域がヒープであることも確認できます。
これはmallocgcの挙動でしょう。
ここで注意すべきは、newobject
では、メモリ領域を確保しているだけで、データは生成していない。という点でしょう。
これが、log.Println(*fuga == nil)
がtrueになる、ようは*fugaがnilである要因だと思います。
では次に後半部です。こちらは複合リテラルでのみ存在する部分です。
0x003b 00059 (hoge.go:10) CALL runtime.makemap_small(SB)
0x0040 00064 (hoge.go:10) PCDATA $2, $1
0x0040 00064 (hoge.go:10) MOVQ (SP), AX
0x0044 00068 (hoge.go:10) PCDATA $2, $-2
0x0044 00068 (hoge.go:10) PCDATA $0, $-2
0x0044 00068 (hoge.go:10) CMPL runtime.writeBarrier(SB), $0
0x004b 00075 (hoge.go:10) JNE 520
0x0051 00081 (hoge.go:10) MOVQ "".hoge+32(SP), CX
0x0056 00086 (hoge.go:10) MOVQ AX, (CX)
一番最初にruntime.makemap_small
が呼ばれています。newobject
同様パラメータとして静的ベースレジスタが渡っています。
makemap_small
の挙動はこちらで確認できます。
分かりやすい関数名のおかげで、この関数でmapの実体を生成していることが推測できます。
また、runtime.writeBarrier
と0を比較して、00520にジャンプしたりなどの分岐や、MOV命令などいくつかありますが、やはりmakemap_small
の存在が&とnewの大きな違いとなっているでしょう。
#結論
今回考察に用いたコードを見るとわかるように、hello
型はmap[int][]int32
型のエイリアスです。
また、&やnewは通常であれば構造体の初期化などに用います。
構造体と比べるとmapは特殊な型です。可変長の連想配列で、宣言時(0初期化前)に値は代入出来ません。
本記事で挙動の考察に至ったのも、元を辿るとこのmapの挙動によるものからです。
本来構造体であれば、makemap_small
はもちろん呼び出されませんし、&でもnewでも見かけ上同じ用に用いることができます。
ただ、扱うモノが構造体であっても、&とnewを比較した場合、newの方が高速に初期化できることが分かりました。newでは領域を確保してるだけだからです。
最後に、一番最初のコードでfuga
をpanicを起こす事なく使用したい場合は、以下のように、makemap_small
を呼び出せる方法で初期化してあげましょう。
package main
import "log"
type hello map[int][]int32
func main() {
var key []int32
hoge := &hello{}
log.Println(hoge)
fuga := new(hello)
log.Println(fuga)
(*hoge)[0] = key
log.Println(hoge)
(*fuga) = map[int][]int32 {0 : key}
log.Println(fuga)
}
map[int][]int32 {0 : key}
で出力されるコード
0x0166 00358 (hoge.go:19) CALL runtime.makemap_small(SB)
0x016b 00363 (hoge.go:19) PCDATA $2, $1
0x016b 00363 (hoge.go:19) MOVQ (SP), AX
0x016f 00367 (hoge.go:19) PCDATA $0, $7
0x016f 00367 (hoge.go:19) MOVQ AX, ""..autotmp_23+48(SP)
0x0174 00372 (hoge.go:19) PCDATA $2, $3
0x0174 00372 (hoge.go:19) LEAQ type.map[int][]int32(SB), CX
0x017b 00379 (hoge.go:19) PCDATA $2, $1
0x017b 00379 (hoge.go:19) MOVQ CX, (SP)
0x017f 00383 (hoge.go:19) PCDATA $2, $0
0x017f 00383 (hoge.go:19) MOVQ AX, 8(SP)
0x0184 00388 (hoge.go:19) MOVQ $0, 16(SP)
0x018d 00397 (hoge.go:19) CALL runtime.mapassign_fast64(SB)
0x0192 00402 (hoge.go:19) PCDATA $2, $6
0x0192 00402 (hoge.go:19) MOVQ 24(SP), DI
0x0197 00407 (hoge.go:19) XORPS X0, X0
0x019a 00410 (hoge.go:19) MOVUPS X0, 8(DI)
0x019e 00414 (hoge.go:19) PCDATA $2, $-2
0x019e 00414 (hoge.go:19) PCDATA $0, $-2
0x019e 00414 (hoge.go:19) CMPL runtime.writeBarrier(SB), $0
0x01a5 00421 (hoge.go:19) JNE 507
0x01a7 00423 (hoge.go:19) MOVQ $0, (DI)
0x01ae 00430 (hoge.go:19) MOVQ ""..autotmp_23+48(SP), AX
0x01b3 00435 (hoge.go:19) MOVQ "".fuga+40(SP), CX
0x01b8 00440 (hoge.go:19) MOVQ AX, (CX)
出力結果
yyyy/mm/dd H:i:s &map[]
yyyy/mm/dd H:i:s &map[]
yyyy/mm/dd H:i:s &map[0:[]]
yyyy/mm/dd H:i:s &map[0:[]]