マルチスレッド2D重力計算


24

私は宇宙探査ゲームを構築しており、現在重力に取り組んでいます(XNAを使用したC#で)。

重力はまだ調整が必要ですが、それを行う前に、物理計算でパフォーマンスの問題に対処する必要があります。

これは100個のオブジェクトを使用しており、通常、物理計算なしで1000個のオブジェクトをレンダリングすると300 FPS(私のFPSキャップ)をはるかに超えますが、10個以上のオブジェクトがゲーム(および実行される単一スレッド)を物理計算を行う際のひざ。

スレッドの使用状況を確認したところ、最初のスレッドはすべての作業から自分自身を殺していたため、別のスレッドで物理計算を行う必要があると考えました。ただし、別のスレッドでGravity.csクラスのUpdateメソッドを実行しようとすると、GravityのUpdateメソッドに何も含まれていなくても、ゲームはまだ2 FPSになります。

Gravity.cs

public void Update()
    {
        foreach (KeyValuePair<string, Entity> e in entityEngine.Entities)
        {
            Vector2 Force = new Vector2();

            foreach (KeyValuePair<string, Entity> e2 in entityEngine.Entities)
            {
                if (e2.Key != e.Key)
                {
                    float distance = Vector2.Distance(entityEngine.Entities[e.Key].Position, entityEngine.Entities[e2.Key].Position);
                    if (distance > (entityEngine.Entities[e.Key].Texture.Width / 2 + entityEngine.Entities[e2.Key].Texture.Width / 2))
                    {
                        double angle = Math.Atan2(entityEngine.Entities[e2.Key].Position.Y - entityEngine.Entities[e.Key].Position.Y, entityEngine.Entities[e2.Key].Position.X - entityEngine.Entities[e.Key].Position.X);

                        float mult = 0.1f *
                            (entityEngine.Entities[e.Key].Mass * entityEngine.Entities[e2.Key].Mass) / distance * distance;

                        Vector2 VecForce = new Vector2((float)Math.Cos(angle), (float)Math.Sin(angle));
                        VecForce.Normalize();

                        Force = Vector2.Add(Force, VecForce * mult);
                    }
                }
            }

            entityEngine.Entities[e.Key].Position += Force;
        }

    }

ええ、知っています。これはネストされたforeachループですが、重力計算を行う他の方法はわかりません。これはうまくいくようで、非常に集中的であるため、独自のスレッドが必要です。(誰かがこれらの計算を行う非常に効率的な方法を知っていても、代わりに複数のスレッドでどのようにそれを行うことができるかを知りたいです)

EntityEngine.cs(Gravity.csのインスタンスを管理します)

public class EntityEngine
{
    public Dictionary<string, Entity> Entities = new Dictionary<string, Entity>();
    public Gravity gravity;
    private Thread T;


    public EntityEngine()
    {
        gravity = new Gravity(this);
    }


    public void Update()
    {
        foreach (KeyValuePair<string, Entity> e in Entities)
        {
            Entities[e.Key].Update();
        }

        T = new Thread(new ThreadStart(gravity.Update));
        T.IsBackground = true;
        T.Start();
    }

}

EntityEngineはGame1.csで作成され、そのUpdate()メソッドはGame1.cs内で呼び出されます。

Gravity.csでの物理計算は、ゲームが更新されるたびに別のスレッドで実行する必要があります。これにより、計算がゲームをひどく低い(0-2)FPSに減速させないようにします。

このスレッドを機能させるにはどうすればよいですか?(改善された惑星重力システムに関する提案は、誰かが持っていれば歓迎します)

また、スレッドを使用しない理由や、誤って使用する危険性についてのレッスンも探していません。それを行う方法についての簡単な答えを探しています。私はすでにこの質問をグーグルで1時間費やしましたが、理解したり助けたりした結果はほとんどありませんでした。私は失礼なことをするつもりはありませんが、まっすぐな意味のある答えを得るのは常にプログラミング初心者として難しいようです。通常、私はそれを理解すれば簡単に問題を解決できるほど複雑な答えを得るのが普通です誰かが私がやりたいことをしてはいけない理由を言って、代替手段を提供しません(それは役に立ちます)。

お手伝いありがとう!

編集:私が得た答えを読んだ後、私はあなたたちが実際に気にし、うまくいくかもしれない答えを吐き出そうとしているだけではないことがわかります。1石で2羽の鳥を殺したかった(パフォーマンスを向上させ、マルチスレッドの基礎を学ぶ)が、問題の大部分は私の計算にあり、スレッド化はパフォーマンスの向上に値するよりも手間がかかるようだ。みなさん、ありがとうございます。もう一度答えを読んで、学校が終わったら解決策を試します。


[上記のアップデートスレッドシステム]は何をしますか(動作しますか)。ところで、ゲームサイクルの中で、たとえばエンティティが更新される前に、できるだけ早く開始します。
ThorinII

2
ネストされたループの内部でのTrig呼び出しは、おそらく最大のヒットです。それらを除去する方法を見つけることができれば、それはkこのO(n^2)問題の多くを減らすでしょう。
–RBarryYoung

1
実際、トリガー呼び出しは完全に不要です。まずベクトルから角度を計算し、次にそれを使用して、指定された方向を指す別のベクトルを生成します。次に、そのベクトルを正規化しますが、sin² + cos² ≡ 1とにかく既に正規化されているためです!関心のある2つのオブジェクトを接続する元のベクトルを使用し、この1つを正規化することもできます。トリガー呼び出しは一切必要ありません。
左周り約

XNAは非推奨ではありませんか?
jcora

@yannbaneその質問は、議論に役立つものを追加しません。いいえ、XNAのステータスは非推奨の定義に適合しません。
セスバティン

回答:


36

ここにあるのは、古典的なO(n²)アルゴリズムです。問題の根本的な原因は、スレッド化とは関係がなく、アルゴリズムが非常に複雑であるという事実に関係しています。

以前に「Big O」表記法に出会ったことがない場合、基本的にn個の要素を操作するために必要な操作の数を意味します(これは非常に簡単な説明です)。100個の要素がループの内部を10000回実行しています。

ゲーム開発では、通常、データ量が少なく(できれば固定または上限がある)、非常に高速なアルゴリズムでない限り、O(n²)アルゴリズムを避けたいと思います。

すべてのエンティティが他のすべてのエンティティに影響を与えている場合、必然的にO(n²)アルゴリズムが必要になります。ただし、実際に相互作用しているのは少数のエンティティだけであるように見えるため(「」に起因if (distance < ...))、「空間パーティション」と呼ばれるものを使用して操作の数を大幅に削減できます。

これはかなり詳細なトピックであり、ゲーム固有のものなので、詳細については新たな質問をすることをお勧めします。次へ移りましょう...


コードの主要なパフォーマンスの問題の1つは非常に単純です。これは非常に遅いです:

foreach (KeyValuePair<string, Entity> e in Entities)
{
    Entities[e.Key].Update();
}

既に持っているオブジェクトについて、文字列による辞書検索を、繰り返しごとに(他のループで複数回)行っています!

これを行うことができます:

foreach (KeyValuePair<string, Entity> e in Entities)
{
    e.Value.Update();
}

または、あなたはこれを行うことができます:(私は個人的にこれが好きです、両方ともほぼ同じ速度でなければなりません)

foreach (Entity e in Entities.Values)
{
    e.Update();
}

文字列による辞書検索はかなり遅いです。直接反復処理は大幅に高速になります。

ただし、実際にアイテムを名前で検索する必要がある頻度はどれくらいですか?それらすべてを反復する必要がある頻度と比較して?まれにしか名前検索を行わない場合は、エンティティをListNameメンバーとして)に保存することを検討してください。

あなたが実際に持っているコードは比較的簡単です。私はそれをプロファイルしていませんが、あなたの実行時間の大部分は繰り返される辞書検索に行くと思います。この問題を修正するだけで、コードは「十分に高速」になります。

編集:次の最大の問題は、おそらく呼び出しAtan2て、すぐにそれをベクトルに変換しSin、そしてCos!ベクトルを直接使用するだけです。


最後に、スレッド化とコードの主要な問題に対処しましょう。

最初に、そして明らかに、フレームごとに新しいスレッドを作成しないでください!スレッドオブジェクトはかなり「重い」ものです。これに対する最も簡単な解決策は、ThreadPool代わりに単純に使用することです。

もちろん、それほど単純ではありません。問題2に移りましょう:2つのスレッドのデータに同時に触れないでください!(適切なスレッドセーフインフラストラクチャを追加せずに。)

あなたは基本的にここで最も恐ろしい方法で記憶を踏み潰しています。ここにはスレッドセーフはありません。gravity.Update開始している複数のスレッドのいずれかが、予期しないときに別のスレッドで使用されているデータを上書きしている可能性があります。一方、メインスレッドは、これらすべてのデータ構造にも触れていることは間違いありません。このコードが再現困難なメモリアクセス違反を引き起こしたとしても、私は驚かないでしょう。

このスレッドセーフのようなものを作成することは難しく多大なパフォーマンスオーバーヘッドを追加する可能性があるため、多くの場合、努力する価値はありません。


しかし、とにかくそれを行う方法について(そうではなく)きちんと尋ねたので、それについて話しましょう...

通常、私はあなたのスレッドが基本的に「火と忘却」である単純な何かを練習することから始めることを勧めます。オーディオの再生、ディスクへの書き込みなど。結果をメインスレッドにフィードバックしなければならない場合、事態は複雑になります。

問題には基本的に3つのアプローチがあります。

1)スレッド間で使用するすべてのデータをロックします。C#では、lockステートメントを使用してこれをかなり簡単にしています。

一般に、new object特定のデータセットを保護するためのロック専用に作成(および保持)します(一般的には、パブリックAPIを記述するときにのみ発生する安全上の理由によりますが、スタイルはすべて同じです)。次に、保護するデータにアクセスするすべての場所でロックオブジェクトをロックする必要があります。

もちろん、使用中のスレッドによって何かが「ロック」され、別のスレッドがそれにアクセスしようとすると、その2番目のスレッドは最初のスレッドが終了するまで待たされます。したがって、並行して実行できるタスクを慎重に選択しない限り、基本的にはシングルスレッドのパフォーマンス(またはそれ以上)が得られます。

したがって、あなたの場合、エンティティコレクションに影響を与えない他のコードが並行して実行されるようにゲームを設計できない限り、これを行う意味はありません。

2)データをスレッドにコピーして処理させ、終了したら結果を再度取り出します。

正確にこれを実装する方法は、何をしているのかに依存します。しかし、明らかにこれには潜在的に高価なコピー操作(または2つ)が含まれ、多くの場合、シングルスレッドで行うよりも遅くなります。

もちろん、バックグラウンドで他の作業を行う必要があります。そうしないと、メインスレッドが座って他のスレッドが終了するのを待って、データをコピーして戻すことができます。

3)スレッドセーフなデータ構造を使用します。

これらは、シングルスレッドの対応物よりもかなり遅く、多くの場合、単純なロックよりも使用が困難です。慎重に使用しない限り、ロックの問題(単一スレッドまでのパフォーマンスの低下)が引き続き発生する可能性があります。


最後に、これはフレームベースのシミュレーションであるため、フレームをレンダリングしてシミュレーションを続行できるように、メインスレッドが他のスレッドの結果を待機するようにする必要があります。完全な説明はここで入れて本当にあまりにも長いですが、基本的には、使用する方法を学習したいと思うMonitor.WaitMonitor.Pulseこれはあなたを始めるための記事です


特定の実装の詳細(最後のビットを除く)またはこれらのアプローチのコードを提供していないことを知っています。まず第一に、カバーすることがたくさんあります。そして、第二に、それらのどれもあなた自身のコードに適用できません-あなたはスレッドを追加することを目指してアーキテクチャ全体にアプローチする必要があります。

スレッディングは、そこにあるコードを魔法のように速くすることはありません-同時に何か他のことをすることができます!


8
可能であれば+10。ここで核心的な問題を要約しているので、最後の文を序文として先頭に移動することができます。別のスレッドでコードを実行しても、同時に他に何もする必要がない場合、魔法のようにレンダリングが高速化されません。そして、レンダラーはおそらくスレッドが終了するのを待ちますが、スレッドが終了しない場合(そしてどのように知ることができますか?)、まだ更新されていないエンティティ物理学と矛盾したゲーム状態を描画します。
LearnCocos2D

スレッド化は私が必要とするものではないことを完全に確信しています。長くて知識のある情報をありがとう!パフォーマンスの改善については、ユーザー(および他のユーザー)が提案した変更を行いましたが、60を超えるオブジェクトを処理する場合、依然としてパフォーマンスが低下しています。N-Bodyシミュレーションの効率性に焦点を当てた別の質問をするのが最善だと思います。ただし、これに対する私の答えは得られます。ありがとう!
郵便配達員

1
よろしくお願いします:)新鮮な質問を投稿するときは、ここにリンクをドロップしてください。そうすれば、私と他の人がそれを見ることができます。
アンドリューラッセル

@Postmanこの答えが一般的に言っていることには同意しますが、これは基本的にスレッド化を利用するPERFECTアルゴリズムであるという事実を完全に見逃していると思います。彼らがGPUでこのようなことをするのには理由があり、それは書き込みを2番目のステップに移動する場合、それが自明な並列アルゴリズムだからです。ロック、コピー、またはスレッドセーフなデータ構造は必要ありません。シンプルなParallel.ForEachで、問題なく完了しました。
チューイーガムボール

@ChewyGumball非常に有効なポイント!また、Postmanはアルゴリズムを2フェーズにする必要がありますが、とにかく2フェーズにする必要があります。ただし、Parallelオーバーヘッドがないわけではないことを指摘しておく価値があります。したがって、特にこのような小さなデータセットや(あるべき)比較的高速なコードの場合、プロファイルすることは間違いありません。そして、もちろん、この場合のアルゴリズムの複雑さを減らすことは、単純に並列処理を行うよりも、おそらく間違いなく優れています。
アンドリューラッセル

22

一見、試してみるべきことがいくつかあります。最初に衝突チェックを減らすようにしてください。quadtreeのような何らかの空間構造を使用してこれを行うことができます。これにより、最初のエンティティを閉じるエンティティのみをクエリするため、2番目のforeachカウントを減らすことができます。

スレッドについて:更新のたびにスレッドを作成しないようにしてください。このオーバーヘッドは、速度を上げるよりも速度を遅くしている可能性があります。代わりに、単一のコリジョンスレッドを作成してみてください。私は具体的なコピー-貼り付け-このコードのアプローチはありませんが、スレッド同期とC#のバックグラウンドワーカーに関する記事があります。

もう1つのポイントは、foreachループentityEngine.Entities[e.Key].Textureで、foreachヘッダーのdictに既にアクセスしているため、実行する必要がないことです。代わりに、単に書くことができますe.Texture。私はこれの影響について本当に知りません、ただあなたに知らせたかったです;)

最後に、1つ目と2つ目のforeachループでクエリされるため、すべてのエンティティをダブルチェックしています。

2つのエンティティAとBの例:

pick A in first foreach loop
   pick A in second foreach loop
      skip A because keys are the same
   pick B in second foreach loop
      collision stuff
pick B in first foreach loop
   pick A in second foreach loop
      collision stuff
   pick B in second foreach loop
      skip B because keys are the same

これは可能なアプローチですが、衝突チェックの半分をスキップして、AとBを1ターンで処理できます

これで開始できることを願っています=)

PS:あなたはそれを聞きたくないと言ったとしても:同じスレッドで衝突検出を維持し、十分に高速化するようにしてください。スレッド化は良いアイデアのように思えますが、これには地獄のように同期する必要があります。衝突チェックが更新より遅い場合(スレッド化の理由)、船がすでに移動した後に衝突がトリガーされ、その逆も同様であるため、グリッチとエラーが発生します。私はあなたを落胆させたくありません、これは単なる個人的な経験です。

EDIT1:QuadTreeチュートリアル(Java)とのリンク:http ://gamedev.tutsplus.com/tutorials/implementation/quick-tip-use-quadtrees-to-detect-likely-collisions-in-2d-space/


10
重力シミュレーションにクワッド/オクツリーを使用することの良い点は、遠くの粒子を無視する代わりに、ツリーの各ブランチにすべての粒子の総質量と重心を保存し、これを使用して平均重力効果を計算できることですこの分岐内のすべての粒子の、他の遠くの粒子上の。これはとして知られているバーンズ・ハットアルゴリズム、およびそれはプロが使うものです
イルマリカロネン

10

正直なところ、最初にすべきことは、より良いアルゴリズムに切り替えることです。

シミュレーションを並列化すると、可能な限り最良の場合でも、CPUの数×CPUあたりのコア数×システムで使用可能なコアあたりのスレッド数に等しい係数で高速化できます。つまり、最新のPCの場合は4〜16です。(GPUにコードを移動すると、開発の複雑さが増し、スレッドごとのベースライン計算速度が低下しますが、はるかに優れた並列化係数が得られます。)サンプルコードのようなO(n²)アルゴリズムを使用すると、現在の2〜4倍のパーティクルを使用します。

逆に、より効率的なアルゴリズムに切り替えると、100から10000の係数(純粋に推測による数値)でシミュレーションを簡単に高速化できます。空間的細分割を使用した優れたn体シミュレーションアルゴリズムの時間の複雑さは、O(n log n)にほぼ比例し、「ほぼ線形」であるため、処理可能なパーティクル数の増加とほぼ同じ要因を期待できます。また、それはまだ1つのスレッドのみを使用しているので、その上にまだ並列化の余地があります。

とにかく、他の回答が指摘したように、相互作用する多数の粒子を効率的にシミュレートするための一般的なトリックは、それらを四分木(2D)または八分木(3D)に整理することです。特に、重力をシミュレートするために使用する基本的なアルゴリズムはBarnes–Hutシミュレーションアルゴリズムです。このアルゴリズムでは、クワッド/オクトツリーの各セルに含まれるすべての粒子の総質量(および重心)を保存し、それを使用して、そのセル内の粒子が他の離れた粒子に及ぼす平均重力効果を近似します。

GooglingによるBarnes–Hutアルゴリズムの説明とチュートリアルはたくさんありますがここから始めましょう。これは、銀河の衝突のGPUシミュレーションに使用れる高度な実装の説明です


6

スレッドとは関係のない別の最適化の答え。ごめんなさい

すべてのペアのDistance()を計算しています。これには、平方根の取得が含まれますが、これには時間がかかります。また、実際のサイズを取得するためのいくつかのオブジェクト検索も含まれます。

代わりにDistanceSquared()関数を使用してこれを最適化できます。任意の2つのオブジェクトが相互作用できる最大距離を事前に計算し、2乗してから、これをDistanceSquared()と比較します。距離の2乗が最大の範囲内にある場合にのみ、平方根を取得して実際のオブジェクトサイズと比較します。

編集:この最適化は、主に衝突をテストしているときに主に使用されますが、実際にはあなたがやっていることではないことに気づきました(ただし、いつかは確実にそうなります)。ただし、すべての粒子のサイズ/質量が類似している場合は、状況に適用できる可能性があります。


うん。この解決策は問題ないかもしれませんが(わずかな精度の損失のみ)、オブジェクトの質量が大きく異なる場合に問題になります。一部のオブジェクトの質量が非常に大きく、一部のオブジェクトの質量が非常に小さい場合、合理的な最大距離はより高くなります。たとえば、小さな塵の粒子に対する地球の重力の影響は、地球では無視できますが、塵の粒子では無視できません(非常に長い距離の場合)。しかし、実際には、同じ距離にある2つのダスト粒子は、互いに大きな影響を与えません。
SDwarfs

実際、それは非常に良い点です。私はこれを衝突テストと誤解していますが、実際には逆のことをしています。粒子が接触していない場合、粒子は互いに影響します。
アリステアバクストン

3

スレッドについてはあまり知りませんが、ループには時間がかかるようですので、これから変更するかもしれません

i = 0; i < count; i++
  j = 0; j < count; j++

  object_i += force(object_j);

これに

i = 0; i < count-1; i++
  j = i+1; j < count; j++

  object_i += force(object_j);
  object_j += force(object_i);

助けることができます


1
なぜそれが役立つのでしょうか?

1
最初の2つのループは10000回の反復を行いますが、2番目のループは4950回の反復のみを行うためです。
ブクシー

1

10個のシミュレートされたオブジェクトでこのような大きな問題を既に抱えている場合は、コードを最適化する必要があります!ネストされたループでは、10 * 10の反復のみが発生し、そのうち10回の反復がスキップされ(同じオブジェクト)、内部ループの90回の反復が発生します。2 FPSしか達成しない場合、これはパフォーマンスが非常に悪く、1秒間に180回の内部ループの反復しか達成できないことを意味します。

次のことをお勧めします。

  1. 準備/ベンチマーク:このルーチンが問題であることを確実に知るには、小さなベンチマークルーチンを作成します。Update()重力のメソッドを複数回、たとえば1000回実行し、その時間を測定します。100個のオブジェクトで30 FPSを達成するには、100個のオブジェクトをシミュレートし、30回の実行時間を測定する必要があります。1秒未満である必要があります。適切な最適化を行うには、このようなベンチマークを使用する必要があります。それ以外の場合は、おそらく反対を達成し、コードはより速くなければならないと思うので、コードの実行を遅くします。

  2. 最適化:O(N²)労力の問題(つまり、シミュレートされたオブジェクトの数Nに応じて計算時間が2次的に増加する)についてはあまりできませんが、コード自体を改善できます。

    a)コード内で多くの「連想配列」(辞書)ルックアップを使用します。これらは遅いです!たとえばentityEngine.Entities[e.Key].Position。ただ使えないのe.Value.Position?これにより、1つのルックアップが保存されます。eとe2によって参照されるオブジェクトのプロパティにアクセスするには、内部ループ全体のどこでもこれを行います...これを変更してください!b)ループ内に新しいベクターを作成しますnew Vector2( .... )。すべての「新しい」呼び出しは、メモリ割り当て(および後で:割り当て解除)を意味します。これらは、辞書の検索よりもはるかに遅いです。このベクターが一時的にのみ必要な場合は、ループの外側に割り当て、新しいオブジェクトを作成するのではなく、値を新しい値に再初期化して再利用します。C)あなたは、例えば(三角関数の多くを使用atan2し、cos)ループ内。精度が本当に正確である必要がない場合は、代わりにルックアップテーブルを使用してみてください。これを行うには、値を定義範囲にスケーリングし、整数値に丸めて、事前に計算された結果の表で調べます。それに関して助けが必要な場合は、質問してください。d)よく使用します.Texture.Width / 2。これを事前に計算し、結果を.Texture.HalfWidthまたはとして保存することができます。これが常に偶数の正の整数値である場合、シフト操作のビットを使用>> 1して2で割ることができます。

一度に1つの変更のみを行い、ベンチマークによって変更を測定して、ランタイムにどのように影響するかを確認してください!たぶん一つのことが良いのに、もう一つのアイデアが悪かったのかもしれません(私が上でそれらを提案しました!)

これらの最適化は、複数のスレッドを使用してパフォーマンスを向上させるよりもはるかに優れていると思います!スレッドを調整するのは面倒なので、他の値を上書きしません。また、同様のメモリ領域にアクセスするときにも競合します。このジョブに4つのCPU /スレッドを使用する場合、フレームレートの2〜3倍の速度しか期待できません。


0

オブジェクト作成ラインなしで再作業できますか?

Vector2 Force = new Vector2();

Vector2 VecForce = new Vector2((float)Math.Cos(angle)、(float)Math.Sin(angle));

毎回2つの新しいオブジェクトを作成する代わりに、エンティティに強制値を配置できる場合は、パフォーマンスの向上に役立つ可能性があります。


4
Vector2XNAの値型です。GCのオーバーヘッドはなく、構築のオーバーヘッドは無視できます。これが問題の原因ではありません。
アンドリューラッセル

@Andrew Russell:確かではありませんが、 "new Vector2"を使用する場合、それでも事実です。「新規」なしでVector2(....)を使用する場合、これはおそらく異なるでしょう。
SDwarfs

1
@stefanK。C#ではできません。新しいものが必要です。C ++を考えていますか?
-MrKWatkins
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.