web-dev-qa-db-ja.com

パフォーマンス:構造体のスライスと構造体へのポインターのスライス

構造体のスライスを頻繁に使用します。このような構造体の例を次に示します。

type MyStruct struct {
    val1, val2, val3    int
    text1, text2, text3 string
    list                []SomeType
}

そこで、スライスを次のように定義します。

[]MyStruct

私はそこに約100万個の要素があり、スライスを大いに使っているとしましょう:

  • 新しい要素を頻繁に追加します。 (要素の総数は不明です。)
  • 私は時々それをソートします。
  • また、要素を削除します(ただし、新しい要素を追加するほどではありません)。
  • 要素を頻繁に読み、それらを(関数の引数として)渡します。
  • 要素自体の内容は変更されません。

私の理解では、これは実際の構造体の多くのシャッフルにつながります。別の方法は、構造体へのポインタのスライスを作成することです:

[]*MyStruct

構造体は現在の場所に残り、フットプリントが小さく、操作が高速になると思われるポインターのみを処理します。しかし、今はガベージコレクターにもっと多くの仕事をさせています。

  • 構造体を直接操作する場合と構造体へのポインターを操作する場合の一般的なガイドラインを提供できますか?
  • GCに任せる作業の量について心配する必要がありますか?
  • 構造体のコピーとポインターのコピーのパフォーマンスのオーバーヘッドは無視できますか?
  • おそらく100万個の要素はそれほど多くありません。スライスがさらに大きくなると、これらすべてがどのように変わりますか(もちろん、RAMに収まります)。
50
Oliver

自分でこれに興味を持ちました。いくつかのベンチマークを実行しました:

type MyStruct struct {
    F1, F2, F3, F4, F5, F6, F7 string
    I1, I2, I3, I4, I5, I6, I7 int64
}

func BenchmarkAppendingStructs(b *testing.B) {
    var s []MyStruct

    for i := 0; i < b.N; i++ {
        s = append(s, MyStruct{})
    }
}

func BenchmarkAppendingPointers(b *testing.B) {
    var s []*MyStruct

    for i := 0; i < b.N; i++ {
        s = append(s, &MyStruct{})
    }
}

結果:

BenchmarkAppendingStructs  1000000        3528 ns/op
BenchmarkAppendingPointers 5000000         246 ns/op

テイクアウェイ:ナノ秒単位です。小さいスライスの場合はおそらく無視できます。しかし、何百万もの操作では、ミリ秒とマイクロ秒の差です。

ところで、私は、基礎となる配列を定期的にコピーするappend()からオーバーヘッドを排除するために、事前に割り当てられたスライス(1000000の容量)でベンチマークを再度実行しようとしました。構造体の追加は1000nsを落としましたが、ポインターの追加はまったく変わりませんでした。

31
Russ Egan

構造体を直接操作する場合と構造体へのポインターを操作する場合の一般的なガイドラインを提供できますか?

いいえ、それはあなたがすでに言及した他のすべての要因に依存しすぎています。

唯一の本当の答えは、ベンチマークと参照です。すべてのケースは異なり、実際のタイミングを調整しても、世界のすべての理論に違いはありません。

(そうは言っても、私の直感ではポインターを使用し、場合によってはsync.Poolガベージコレクターを支援するために: http://golang.org/pkg/sync/#Pool

9
Evan