Shogo's Blog

たぶんプログラミングとかについて書いていくブログ

map[string]Hoge or map[string]*Hoge ?

Go言語でポインタを使うべきか使わないべきか問題。 「ケース・バイ・ケースなので、状況に応じて使い分けましょう!」という結論が出るのは目に見えているので、 具体例について検証してみた結果を書いておきます。

背景

他の人のコードレビューを見ていたら、 レビュアーが「コピーをしないで済むのでstructの受け渡しにはポインタ使ったほうがいいと思います!」とコメントしていて、 そうなのか?と思ったのですがあんまり自信がなかったので検証してみました。 コメントがついていたのは以下のようなコード。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package hoge

import (
  "strconv"
)

type Hoge struct {
  A int
  B int
  C int
}

func NewHogeMapStruct() map[string]Hoge {
  m := make(map[string]Hoge)
  for i := 0; i < 10000; i++ {
      m[strconv.Itoa(i)] = Hoge{i, i, i}
  }
  return m
}

ポイントは以下の点です。

  • 受け渡すstructはintが3つ程度の小さなもの
  • mapに入れて返す

benchmarkを使って検証する

ポインタを使わない版と使う版を両方作ってベンチマークをとってみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package hoge

import (
  "strconv"
)

type Hoge struct {
  A int
  B int
  C int
}

// ポインタ使わない版
func NewHogeMapStruct() map[string]Hoge {
  m := make(map[string]Hoge)
  for i := 0; i < 10000; i++ {
      m[strconv.Itoa(i)] = Hoge{i, i, i}
  }
  return m
}

// ポインタ使う版
func NewHogeMapPointer() map[string]*Hoge {
  m := make(map[string]*Hoge)
  for i := 0; i < 10000; i++ {
      m[strconv.Itoa(i)] = &Hoge{i, i, i}
  }
  return m
}

ベンチマークはこれらの関数をただ呼び出すだけのシンプルなもの。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package hoge

import (
  "strconv"
  "testing"
)

func BenchmarkStruct(b *testing.B) {
  for i := 0; i < b.N; i++ {
      NewHogeMapStruct()
  }
}

func BenchmarkPointer(b *testing.B) {
  for i := 0; i < b.N; i++ {
      NewHogeMapPointer()
  }
}

go test -benchmem -bench . すると以下のような結果が得られました。

1
2
3
4
PASS
BenchmarkStruct        500       3238732 ns/op     1662087 B/op      10615 allocs/op
BenchmarkPointer     500     3485712 ns/op     1372074 B/op      20593 allocs/op
ok    github.com/shogo82148/pointer-test  4.067s

ポインタを使わないほうが若干速いですね。 メモリのアロケーション回数はポインタ使う版の半分です。

ポインタ使わない版は速度・メモリアロケーション回数は減ったものの、 必要なバイト数は増えています。 おそらく、これから値が入る予定のメモリ領域を予め確保しており、 その分のメモリを多めに食っているのでしょう。 ポインタ使う版では構造体の中身を入れる分は必要になったときにnewするので、 使用するメモリは最小限で済みます。

アセンブリを見てみてみる

go tool を使うとアセンブリが見れるらしい。 go tool 6g -S hoge.go を実行してアセンブリも眺めてみます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
--- prog list "NewHogeMapStruct" ---
0000 (hoge.go:13) TEXT    NewHogeMapStruct+0(SB),$72-8
0001 (hoge.go:13) FUNCDATA $0,gcargs·0+0(SB)
0002 (hoge.go:13) FUNCDATA $1,gclocals·0+0(SB)
0003 (hoge.go:13) TYPE    ~anon0+0(FP){map[string]"".Hoge},$8
0004 (hoge.go:13) TYPE    m+-8(SP){map[string]"".Hoge},$8
0005 (hoge.go:13) TYPE    i+-16(SP){int},$8
0006 (hoge.go:14) MOVQ    $type.map[string]"".Hoge+0(SB),(SP)
0007 (hoge.go:14) MOVQ    $0,8(SP)
0008 (hoge.go:14) PCDATA  $0,$24
0009 (hoge.go:14) CALL    ,runtime.makemap+0(SB)
0010 (hoge.go:14) PCDATA  $0,$-1
0011 (hoge.go:14) MOVQ    16(SP),BX
0012 (hoge.go:14) MOVQ    BX,m+-8(SP)
0013 (hoge.go:15) MOVQ    $0,AX
0014 (hoge.go:15) JMP     ,16
0015 (hoge.go:15) INCQ    ,AX
0016 (hoge.go:15) CMPQ    AX,$10000
0017 (hoge.go:15) JGE     $0,40
0018 (hoge.go:16) MOVQ    AX,i+-16(SP)
0019 (hoge.go:16) MOVQ    AX,(SP)
0020 (hoge.go:16) CALL    ,strconv.Itoa+0(SB)
0021 (hoge.go:16) MOVQ    i+-16(SP),DX
0022 (hoge.go:16) MOVQ    8(SP),SI
0023 (hoge.go:16) MOVQ    16(SP),BP
0024 (hoge.go:16) MOVQ    statictmp_0002+0(SB),BX
0025 (hoge.go:16) MOVQ    statictmp_0002+8(SB),BX
0026 (hoge.go:16) MOVQ    statictmp_0002+16(SB),BX
0027 (hoge.go:16) MOVQ    $type.map[string]"".Hoge+0(SB),(SP)
0028 (hoge.go:16) MOVQ    m+-8(SP),BX
0029 (hoge.go:16) MOVQ    BX,8(SP)
0030 (hoge.go:16) MOVQ    SI,16(SP)
0031 (hoge.go:16) MOVQ    BP,24(SP)
0032 (hoge.go:16) MOVQ    DX,32(SP)
0033 (hoge.go:16) MOVQ    DX,40(SP)
0034 (hoge.go:16) MOVQ    DX,48(SP)
0035 (hoge.go:16) PCDATA  $0,$56
0036 (hoge.go:16) CALL    ,runtime.mapassign1+0(SB)
0037 (hoge.go:16) MOVQ    i+-16(SP),AX
0038 (hoge.go:16) PCDATA  $0,$-1
0039 (hoge.go:15) JMP     ,15
0040 (hoge.go:18) MOVQ    m+-8(SP),BX
0041 (hoge.go:18) MOVQ    BX,~anon0+0(FP)
0042 (hoge.go:18) RET     ,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
--- prog list "NewHogeMapPointer" ---
0043 (hoge.go:21) TEXT    NewHogeMapPointer+0(SB),$72-8
0044 (hoge.go:21) FUNCDATA $0,gcargs·1+0(SB)
0045 (hoge.go:21) FUNCDATA $1,gclocals·1+0(SB)
0046 (hoge.go:21) TYPE    ~anon0+0(FP){map[string]*"".Hoge},$8
0047 (hoge.go:21) TYPE    m+-24(SP){map[string]*"".Hoge},$8
0048 (hoge.go:21) TYPE    i+-32(SP){int},$8
0049 (hoge.go:21) TYPE    autotmp_0003+-16(SP){string},$16
0050 (hoge.go:22) MOVQ    $type.map[string]*"".Hoge+0(SB),(SP)
0051 (hoge.go:22) MOVQ    $0,8(SP)
0052 (hoge.go:22) PCDATA  $0,$24
0053 (hoge.go:22) CALL    ,runtime.makemap+0(SB)
0054 (hoge.go:22) PCDATA  $0,$-1
0055 (hoge.go:22) MOVQ    16(SP),BX
0056 (hoge.go:22) MOVQ    BX,m+-24(SP)
0057 (hoge.go:23) MOVQ    $0,AX
0058 (hoge.go:23) JMP     ,60
0059 (hoge.go:23) INCQ    ,AX
0060 (hoge.go:23) CMPQ    AX,$10000
0061 (hoge.go:23) JGE     $0,94
0062 (hoge.go:24) MOVQ    AX,i+-32(SP)
0063 (hoge.go:24) MOVQ    AX,(SP)
0064 (hoge.go:24) CALL    ,strconv.Itoa+0(SB)
0065 (hoge.go:24) MOVQ    8(SP),BX
0066 (hoge.go:24) MOVQ    BX,autotmp_0003+-16(SP)
0067 (hoge.go:24) MOVQ    16(SP),BX
0068 (hoge.go:24) MOVQ    BX,autotmp_0003+-8(SP)
0069 (hoge.go:24) MOVQ    $type."".Hoge+0(SB),(SP)
0070 (hoge.go:24) PCDATA  $0,$16
0071 (hoge.go:24) CALL    ,runtime.new+0(SB)
0072 (hoge.go:24) MOVQ    i+-32(SP),CX
0073 (hoge.go:24) PCDATA  $0,$-1
0074 (hoge.go:24) MOVQ    8(SP),AX
0075 (hoge.go:24) NOP     ,
0076 (hoge.go:24) MOVQ    CX,(AX)
0077 (hoge.go:24) NOP     ,
0078 (hoge.go:24) MOVQ    CX,8(AX)
0079 (hoge.go:24) NOP     ,
0080 (hoge.go:24) MOVQ    CX,16(AX)
0081 (hoge.go:24) MOVQ    $type.map[string]*"".Hoge+0(SB),(SP)
0082 (hoge.go:24) MOVQ    m+-24(SP),BX
0083 (hoge.go:24) MOVQ    BX,8(SP)
0084 (hoge.go:24) MOVQ    autotmp_0003+-16(SP),BX
0085 (hoge.go:24) MOVQ    BX,16(SP)
0086 (hoge.go:24) MOVQ    autotmp_0003+-8(SP),BX
0087 (hoge.go:24) MOVQ    BX,24(SP)
0088 (hoge.go:24) MOVQ    AX,32(SP)
0089 (hoge.go:24) PCDATA  $0,$40
0090 (hoge.go:24) CALL    ,runtime.mapassign1+0(SB)
0091 (hoge.go:24) MOVQ    i+-32(SP),AX
0092 (hoge.go:24) PCDATA  $0,$-1
0093 (hoge.go:23) JMP     ,59
0094 (hoge.go:26) MOVQ    m+-24(SP),BX
0095 (hoge.go:26) MOVQ    BX,~anon0+0(FP)
0096 (hoge.go:26) RET     ,

メモリアロケーションが起きているのはおそらく runtime.newstrconv.Itoa を呼び出している部分でしょう。 ポインタ使う版では両方とも呼び出していますが、ポインタ使わない版ではstrconv.Itoaの呼び出しだけです。 ポインタ使う版ではmapのkeyとvalueのメモリ領域をそれぞれ確保が必要なのに対して、 ポインタ使わない版ではvalueのためのメモリ領域をnew(map[string]Hoge)の時点で一括確保するので、 メモリアロケーションが少なくて済むということですね。

結論

今回の場合構造体のサイズが小さいく、コピーのコスト<アロケーションのコストであったため、 速度的にはポインタを使わない方が有利でした。 しかし、mapは値の入っていない要素分を予め確保するので、 メモリ使用量的にはポインタを使う方が有利でした。 結局は速度とメモリ使用量のトレードオフということです。

実際のコードでは、キーの個数は60個程度で呼び出される頻度もそんなに多くなく、 速度もメモリも十分に足りるので、正直どっちでも良かった気がします。 ポインタを使わないほうがタイプ数がちょっと減ってコード書くときに少し嬉しいくらいですかね。

結論の結論

ケース・バイ・ケースなので、状況に応じて使い分けましょう!

Comments