質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.31%
Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

データ構造

データ構造とは、データの集まりをコンピュータの中で効果的に扱うために、一定の形式に系統立てて格納する形式を指します。(配列/連想配列/木構造など)

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

Q&A

1回答

1434閲覧

Go言語の構造体へのアクセス方法による効率の違い(ポインタor実体)

mask_mus

総合スコア37

Go

Go(golang)は、Googleで開発されたオープンソースのプログラミング言語です。

データ構造

データ構造とは、データの集まりをコンピュータの中で効果的に扱うために、一定の形式に系統立てて格納する形式を指します。(配列/連想配列/木構造など)

ポインタ

ポインタはアドレスを用いてメモリに格納された値を"参照する"変数です。

0グッド

1クリップ

投稿2021/03/07 05:35

Go言語における構造体のフィールドへのアクセス方法には、ドット(.)を使用した場合、次の二通りがあると思います。

go

1type Node struct { 2 id int 3} 4 5func main() { 6 n := Node{} 7 n.id = 0 8 9 m := &Node{} 10 m.id = 0 11}

この二つの方法の性能を測るベンチマークを次のコードで実行しました。

go

1package test 2 3import ( 4 "testing" 5) 6 7type Node struct { 8 id int 9} 10 11const C = 10000 12 13func BenchmarkA(b *testing.B) { 14 b.ResetTimer() 15 for i := 0; i < b.N; i++ { 16 n := Node{} 17 n.id = 0 18 } 19} 20 21func BenchmarkB(b *testing.B) { 22 b.ResetTimer() 23 for i := 0; i < b.N; i++ { 24 n := &Node{} 25 n.id = 0 26 } 27} 28

以下のような結果が出ました。数回計測しましたが、どれもほぼ同じ結果でした。(ns/opに0.01ほどの差)

$ go test -bench . -benchmem -gcflags '-N -l' goos: linux goarch: amd64 BenchmarkA-6 1000000000 0.993 ns/op 0 B/op 0 allocs/op BenchmarkB-6 801381580 1.49 ns/op 0 B/op 0 allocs/op

実体からフィールドにアクセスする場合とポインタからアクセスする場合で、なぜ速度が変わるのでしょうか?

次に、構造体そのものをスライスにappendする場合と構造体のポインタをappendする場合の差を調べるために、以下のコードでベンチマークを行いました。

go

1package test 2 3import "testing" 4 5type Node struct { 6 id int 7} 8 9const C = 10000 10 11func BenchmarkA(b *testing.B) { 12 b.ResetTimer() 13 for i := 0; i < b.N; i++ { 14 l := []Node{} 15 for j := 0; j < C; j++ { 16 n := Node{} 17 l = append(l, n) 18 } 19 } 20} 21 22func BenchmarkB(b *testing.B) { 23 b.ResetTimer() 24 for i := 0; i < b.N; i++ { 25 l := []*Node{} 26 for j := 0; j < C; j++ { 27 n := &Node{} 28 l = append(l, n) 29 } 30 } 31}

結果は以下のようになりました。

$ go test -bench . -benchmem -gcflags '-N -l' goos: linux goarch: amd64 BenchmarkA-6 22455 53447 ns/op 386301 B/op 20 allocs/op BenchmarkB-6 5648 192217 ns/op 466298 B/op 10020 allocs/op

ポインタをappendする方が、メモリ割り当て回数が非常に多くなっていて、実行時間も増えています。
どこでこのような差が生まれているのでしょうか?

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答1

0

単純にヒープメモリ操作回数がメモリ割り当て回数にそのまま出ています。

  • 最初にスライスを確保した場合1回のメモリ割り当てが発生します
  • スライスをゼロサイズからGrowさせていく場合、1,2,4,8...とスライスのメモリ拡張処理が発生します(この場合19回)
  • A:Node{}はスタックに設けられたスペースに初期化されスライスが持つメモリにコピーされる
  • B:&Node{}の結果をコンテナに投入する場合スタックは利用せずヒープからnew(Node)と等価処理します。(=10000回割り当て)

ベンチマークコードのlの初期値を以下の様に書くと、スライスのメモリ拡張処理がなくなります。

go

1l := make([]Node, 0, C)

go

1l := make([]*Node, 0, C)

結果の例

BenchmarkA-4 59569 23897 ns/op 81920 B/op 1 allocs/op BenchmarkB-4 3640 304698 ns/op 161920 B/op 10001 allocs/op PASS

ポインタ型をnewする以上、メモリ割り当てを回避するのは難しいです。
(Goのコンパイラがスタック割り当てで済むと判断した時だけスタックになります)

例示の状況であれば「非ポインタ型構造体」のほうが良いことだらけに見えます。ただ、スライスにappendする時にコピーするのは「ポインタ値のサイズ(CPUレジスタ1個分)」のみです。非ポインタ型構造体をスライスにappendする時にコピーするのは「構造体全体のサイズ」です。また、引数渡しや返値渡し、メソッドレシーバーアクセスのたびに「非ポインタ型構造体」は「構造体全体のコピー」が発生します。

つまり、構造体定義のサイズでトレードオフが発生します。あるていど構造体定義が肥大化すると利点の効果よりもコピーコストの方が問題として大きくなります。

また、インターフェース型として取り回す際、非ポインタ型の場合はいくらか制約が発生します。速度問題がクリティカルであると判断するまではほとんどのケースでポインタ型を使っておく方が無難ではあります。

僕の場合、「とりあえずポインタ型で実装ー>GCで遅い問題がでたらsync.Poolなどで確保メモリの再利用を検討する」という流れが多いです。最初から非ポインタ構造体で設計するのはフィールド内容が定義上不変である場合に限ります(2D/3Dベクトルなど)。

投稿2021/03/08 00:18

編集2021/03/08 00:26
nobonobo

総合スコア3367

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

nobonobo

2021/03/08 00:42

メモリ再利用には再利用特有の課題もあるので注意が必要ですが。(初期化の責任が別途発生する)
mask_mus

2021/03/08 05:50

前回に引き続きご回答いただきありがとうございます。 3つほど追加で質問があります。 ・A:Node{}はスタックに設けられたスペースに初期化されスライスが持つメモリにコピーされる、についてですが、この場合、コピー先のスライスのメモリはスタックになるのでしょうかヒープになるのでしょうか? ・一つ目のベンチマークについてですが、ヒープの割り当てはどちらも行われていないように見えるのですが、速度の差はどこで生じるのでしょうか? ・非ポインタ構造体のレシーバとポインタ構造体のレシーバでは速度に差はあるのでしょうか?
nobonobo

2021/03/08 08:40

ひとつめ:スタックに初期化されヒープにあるスライス用の配列メモリにコピーされます。 ふたつめ:短命なオブジェクトはスタックに置いても大丈夫という判断をコンパイラがそう判断します。 みっつめ:メソッドコールにはレシーバのコピーが発生します。レシーバがポインタ型ならポインタだけのコピーで済みます。
mask_mus

2021/03/08 12:00

何度も質問してしまい申し訳ないのですが、 n:=Node{} n.id=0 と n:=&Node{} n.id=0 のフィールドのアクセス方法に、性能的な違いはあるのでしょうか?
nobonobo

2021/03/08 12:13

アセンブリコードは読めますか?極端な速度チューニングをするには必要になる知識です。 もし読めないならそちらの学習をしてみることをお勧めします。 go tool compile -S sample_test.go > asm.S などとするとアセンブリコードが出力されます。こちらを読めばどちらがどう違うか大して差がないのかは一目瞭然です。 個人的には回答の後半に示した通りポインタ型でまずは組みましょう。 そして、実際のアプリケーションでプロファイルを録って、 最もクリティカルなところだけにチューニングを検討しましょう。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

まだベストアンサーが選ばれていません

会員登録して回答してみよう

アカウントをお持ちの方は

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.31%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問