35

私はレイトレーサーの趣味のプロジェクトをやっていて、もともとはベクターオブジェクトとレイオブジェクトに構造体を使用していました。レイトレーサーはそれらを使用するのに最適な状況だと思いました。方法、彼らは軽量です。ただし、VectorとRayで「struct」を「class」に変更するだけで、パフォーマンスが大幅に向上しました。

何が得られますか?それらは両方とも小さく(ベクターの場合は3つのフロート、レイの場合は2つのベクター)、過度にコピーされません。もちろん、必要に応じてメソッドに渡しますが、それは避けられません。では、構造体を使用するときにパフォーマンスを低下させる一般的な落とし穴は何ですか?次のようなMSDNの記事を読みました。

この例を実行すると、構造体ループが桁違いに高速であることがわかります。ただし、ValueTypeをオブジェクトのように扱う場合は、ValueTypeの使用に注意することが重要です。これにより、プログラムに余分なボクシングとアンボクシングのオーバーヘッドが追加され、オブジェクトに固執した場合よりもコストがかかる可能性があります。これが実際に動作することを確認するには、上記のコードを変更して、fooとbarの配列を使用します。パフォーマンスはほぼ同等であることがわかります。

しかし、それはかなり古く(2001)、「それらを配列に入れると、ボクシング/アンボクシングが発生する」という全体が奇妙に感じました。本当?ただし、一次光線を事前に計算して配列に入れたので、この記事を取り上げて、必要なときに一次光線を計算し、配列に追加することはありませんでしたが、何も変わりませんでした。クラスでは、それでも1.5倍高速でした。

私は.NET3.5SP1を実行していますが、これにより、構造体メソッドがインライン化されなかった問題が修正されたと思います。

つまり、基本的に、ヒント、考慮すべきこと、避けるべきことはありますか?

編集:いくつかの回答で示唆されているように、私は構造体を参照として渡すことを試みたテストプロジェクトを設定しました。2つのベクトルを追加する方法:

public static VectorStruct Add(VectorStruct v1, VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static VectorStruct Add(ref VectorStruct v1, ref VectorStruct v2)
{
  return new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

public static void Add(ref VectorStruct v1, ref VectorStruct v2, out VectorStruct v3)
{
  v3 = new VectorStruct(v1.X + v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
}

それぞれについて、次のベンチマーク方法のバリエーションを取得しました。

VectorStruct StructTest()
{
  Stopwatch sw = new Stopwatch();
  sw.Start();
  var v2 = new VectorStruct(0, 0, 0);
  for (int i = 0; i < 100000000; i++)
  {
    var v0 = new VectorStruct(i, i, i);
    var v1 = new VectorStruct(i, i, i);
    v2 = VectorStruct.Add(ref v0, ref v1);
  }
  sw.Stop();
  Console.WriteLine(sw.Elapsed.ToString());
  return v2; // To make sure v2 doesn't get optimized away because it's unused. 
}

すべてがほぼ同じように機能するようです。JITによって、この構造体を渡すための最適な方法に最適化される可能性はありますか?

EDIT2:ちなみに、テストプロジェクトで構造体を使用すると、クラスを使用するよりも約50%高速になることに注意する必要があります。なぜこれが私のレイトレーサーと違うのかわかりません。

4

12 に答える 12

29

構造体の配列は、メモリ内の単一の連続した構造になりますが、オブジェクトの配列 (参照型のインスタンス) 内の項目は、ポインター (つまり、ガベージ コレクション ヒープ上のオブジェクトへの参照) によって個別にアドレス指定する必要があります。したがって、アイテムの大規模なコレクションを一度に扱う場合、構造体は必要な間接参照が少ないため、パフォーマンスが向上します。さらに、構造体は継承できないため、コンパイラは追加の最適化を行うことができます (ただし、これは単なる可能性であり、コンパイラに依存します)。

ただし、構造体の代入セマンティクスはまったく異なり、継承もできません。したがって、必要に応じて特定のパフォーマンス上の理由を除いて、通常は構造体を避けます。


構造体

構造体 (値型) によってエンコードされた値 v の配列は、メモリ内では次のようになります。

vvvv

クラス

クラス (参照型) によってエンコードされた値 v の配列は、次のようになります。

pppp

..v.v.vv.

ここで、p は、ヒープ上の実際の値 v を指す this ポインターまたは参照です。ドットは、ヒープ上に散在している可能性がある他のオブジェクトを示します。参照型の場合、対応する p を介して v を参照する必要があります。値型の場合、配列内のオフセットを介して値を直接取得できます。

于 2009-02-28T15:46:17.000 に答える
12

いつ構造体を使用するかに関する推奨事項では、16 バイトを超えてはならないことが示されています。あなたの Vector は 12 バイトで、これは限界に近づいています。レイには 2 つのベクトルがあり、24 バイトに設定されています。これは明らかに推奨される制限を超えています。

構造体が 16 バイトを超えると、単一の命令セットでは効率的にコピーできなくなり、代わりにループが使用されます。したがって、この「魔法の」制限を渡すことで、実際には、オブジェクトへの参照を渡すときよりも構造体を渡すときに、より多くの作業を行うことになります。これが、オブジェクトを割り当てるときにオーバーヘッドが増えるにもかかわらず、クラスを使用するとコードが高速になる理由です。

Vector は引き続き構造体である可能性がありますが、Ray は単純に大きすぎて構造体としてうまく機能しません。

于 2009-02-28T04:04:24.410 に答える
9

.NET ジェネリックより前のボックス化/ボックス化解除に関して書かれたものはすべて、一粒の塩で取ることができます。ジェネリック コレクション型により、値型のボックス化とボックス化解除の必要がなくなりました。これにより、これらの状況で構造体を使用する価値が高まります。

特定の速度低下については、おそらくいくつかのコードを確認する必要があります。

于 2009-02-28T01:12:22.953 に答える
7

基本的に、それらを大きくしすぎず、可能な場合は ref で渡します。私はこれをまったく同じ方法で発見しました... Vector および Ray クラスを構造体に変更することによって。

より多くのメモリが渡されると、キャッシュのスラッシングが発生します。

于 2009-02-28T01:12:51.733 に答える
7

その鍵は、あなたの投稿からの次の 2 つのステートメントにあると思います。

あなたは何百万ものそれらを作成します

もちろん、必要に応じてそれらをメソッドに渡します

構造体のサイズが 4 バイト (または 64 ビット システムの場合は 8 バイト) 以下でない限り、単純にオブジェクト参照を渡した場合よりも、各メソッド呼び出しでより多くをコピーすることになります。

于 2009-02-28T01:17:54.623 に答える
6

私が最初に探すのは、EqualsとGetHashCodeを明示的に実装していることを確認することです。これを行わないと、これらのそれぞれのランタイム実装は、2つの構造体インスタンスを比較するために非常にコストのかかる操作を実行することを意味します(内部的には、リフレクションを使用して各プライベートフィールドを決定し、それらが等しいかどうかをチェックします。これにより、かなりの量の割り当てが発生します) 。

ただし、一般的には、プロファイラーの下でコードを実行し、遅い部分がどこにあるかを確認するのが最善の方法です。それは目を見張るような経験になる可能性があります。

于 2009-02-28T03:12:03.150 に答える
4

アプリケーションのプロファイルを作成しましたか? プロファイリングは、実際のパフォーマンスの問題がどこにあるかを確認する唯一の確実な方法です。構造体では一般的に良い/悪い操作がありますが、プロファイリングしない限り、問題が何であるかを推測するだけです。

于 2009-02-28T01:40:30.980 に答える
2

機能は似ていますが、構造は通常、クラスよりも効率的です。型が参照型よりも値型として優れたパフォーマンスを発揮する場合は、クラスではなく構造を定義する必要があります。

具体的には、構造タイプは次のすべての基準を満たす必要があります。

  • 単一の値を論理的に表す
  • インスタンスサイズが16バイト未満
  • 作成後に変更されることはありません
  • 参照型にキャストされません
于 2009-04-17T15:00:26.287 に答える
0

私自身のレイ トレーサーも構造体のベクトル (レイではありません) を使用しており、ベクトルをクラスに変更してもパフォーマンスに影響はないようです。現在、ベクトルに 3 つの double を使用しているため、本来よりも大きくなる可能性があります。ただし、これは明らかかもしれませんが、私にとってはそうではありませんでした。それは、Visual Studio の外でプログラムを実行することです。最適化されたリリース ビルドに設定しても、VS の外部で exe を起動すると、大幅な速度向上が得られます。ベンチマークを行う場合は、これを考慮に入れる必要があります。

于 2009-04-17T10:13:03.887 に答える
-1

構造体が小さく、一度に存在するものが多すぎない場合は、ヒープではなくスタックに配置する必要があります(ローカル変数であり、クラスのメンバーではない場合)。これは、GCが存在しないことを意味します。 tを呼び出す必要があり、メモリの割り当て/割り当て解除はほぼ瞬時に行われる必要があります。

構造体をパラメーターとして関数に渡すと、構造体がコピーされます。これは、より多くの割り当て/割り当て解除(スタックから、ほぼ瞬時ですが、オーバーヘッドがあります)だけでなく、2つのコピー間でデータを転送するだけのオーバーヘッドを意味します。 。参照を介して渡す場合、データをコピーするのではなく、どこからデータを読み取るかを指示するだけなので、これは問題ではありません。

これについては100%確信はありませんが、スタック上のメモリはそのために予約されており、スタックとしてコピーする必要がないため、「out」パラメータを介して配列を返すことで速度が向上する可能性があると思います関数呼び出しの最後で「巻き戻され」ます。

于 2009-02-28T16:14:15.420 に答える
-5

構造体を Nullable オブジェクトにすることもできます。カスタムクラスは作成できません

なので

Nullable<MyCustomClass> xxx = new Nullable<MyCustomClass>

構造体がnull可能である場所

Nullable<MyCustomStruct> xxx = new Nullable<MyCustomStruct>

しかし、(明らかに) すべての継承機能を失うことになります

于 2009-02-28T01:26:43.213 に答える