LoginSignup
28
19

More than 5 years have passed since last update.

Go言語での初期化における&とnewの挙動の違い

Last updated at Posted at 2019-05-03

問題提起

Goでは&とnewの初期化に関して、一行で初期化できるかどうか以外の違いは無いという文面をよく見ますが本当にそうでしょうか?

考察

goプログラムによる検証

hoge.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

と、パニックが起きてしまいます。
一つずつ挙動を追ってみましょう。hogefugaそれぞれ宣言の後に以下の2行を追加してみます。

hoge.go
//上記省略
    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以前)はこのようになっています。
スクリーンショット 2019-05-03 16.37.29.png
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を呼び出せる方法で初期化してあげましょう。

hoge.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) = 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:[]]
28
19
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
28
19