Go言語でポインタを使うべきか使わないべきか問題。 「ケース・バイ・ケースなので、状況に応じて使い分けましょう!」という結論が出るのは目に見えているので、 具体例について検証してみた結果を書いておきます。
背景
他の人のコードレビューを見ていたら、 レビュアーが「コピーをしないで済むのでstructの受け渡しにはポインタ使ったほうがいいと思います!」とコメントしていて、 そうなのか?と思ったのですがあんまり自信がなかったので検証してみました。 コメントがついていたのは以下のようなコード。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
ポイントは以下の点です。
- 受け渡す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 |
|
ベンチマークはこれらの関数をただ呼び出すだけのシンプルなもの。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
go test -benchmem -bench .
すると以下のような結果が得られました。
1 2 3 4 |
|
ポインタを使わないほうが若干速いですね。 メモリのアロケーション回数はポインタ使う版の半分です。
ポインタ使わない版は速度・メモリアロケーション回数は減ったものの、 必要なバイト数は増えています。 おそらく、これから値が入る予定のメモリ領域を予め確保しており、 その分のメモリを多めに食っているのでしょう。 ポインタ使う版では構造体の中身を入れる分は必要になったときに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 |
|
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 |
|
メモリアロケーションが起きているのはおそらく runtime.new
と strconv.Itoa
を呼び出している部分でしょう。
ポインタ使う版では両方とも呼び出していますが、ポインタ使わない版ではstrconv.Itoa
の呼び出しだけです。
ポインタ使う版ではmapのkeyとvalueのメモリ領域をそれぞれ確保が必要なのに対して、
ポインタ使わない版ではvalueのためのメモリ領域をnew(map[string]Hoge)
の時点で一括確保するので、
メモリアロケーションが少なくて済むということですね。
結論
今回の場合構造体のサイズが小さいく、コピーのコスト<アロケーションのコストであったため、
速度的にはポインタを使わない方が有利でした。
しかし、map
は値の入っていない要素分を予め確保するので、
メモリ使用量的にはポインタを使う方が有利でした。
結局は速度とメモリ使用量のトレードオフということです。
実際のコードでは、キーの個数は60個程度で呼び出される頻度もそんなに多くなく、 速度もメモリも十分に足りるので、正直どっちでも良かった気がします。 ポインタを使わないほうがタイプ数がちょっと減ってコード書くときに少し嬉しいくらいですかね。
結論の結論
ケース・バイ・ケースなので、状況に応じて使い分けましょう!