不変性がゲームエンティティ(常に動き回ったり変化している)やパーティクルシステムなどの非常に動的なオブジェクトや、パーティクルシステムに適しているかどうかを確認しようとします。理論的にも可能かどうか疑問に思っています。
このシナリオで高性能の不変データ構造を作成する方法を教えてください。
通常、最大のパフォーマンスを得るには、GPUにプッシュして並列処理を実行する可変配列があります。同じか、同じパフォーマンスに近い、この問題に対する機能的/永続的なデータストレージ/不変の解決策があるかどうかと思います。
具体的には、不変のソリューションでは、不変のデータ構造に絶えず追加することでメモリが不足するように見えます。データ構造の履歴/バージョニングの量を制限する必要があるようです。つまり、不変性が制限されているだけです。次に、これらの新しいオブジェクトを作成するオーバーヘッドが多すぎるように見えますが、おそらく、これを無視できるようにするポインタまたは何かを使用できる低レベルのデータ構造があります。
不変データ構造は、次のようなシステムに最適です。
アプリケーションによって管理されるデータの大部分は変更されないままです(または、変更されないようにモデル化できます)
データへの変更は、以前の状態に対する一連の小さな違いとして適用またはモデル化できます。
私は過去に自分で動的パーティクルシステムを実装していませんでしたが、これらのシステムについて知っていることから、これらはそのカテゴリには当てはまりませんです。パーティクルシステムでは、パーティクルの位置や速度などの膨大な量のデータが、メインループの1つの反復から次の反復まで完全に変更されます。
パーティクルの状態を表すために「不変の配列」を使用し、GPUを使用してそれを処理すると、この操作が「その場で」適用されず、結果が新しい配列として作成されるソリューションにつながります。次のステップが前の処理サイクルから元の配列を破棄することである場合、不変性のメリットはありません-そのような操作をインプレースで実行すると、中間結果を使用するよりも少なくともメモリ効率が2倍速くなりますアレイ。
常に動き回る不変のデータをいくつか示します。
竜は常にフォロースルーの仕方を知っていました。ここでわかるように、データが不変だからといって、何も変更されていないわけではありません。
具体的には、不変のソリューションでは、不変のデータ構造に絶えず追加することでメモリが不足するように見えます。データ構造の履歴/バージョニングの量を制限する必要があるようです。つまり、不変性が制限されているだけです。
これは、ほとんどの場合抽象化できるリソースの問題です。状態を不変に変更するすべての要求を記録し、それらの状態変更要求をすべて再生することで、状態を計算したものにすることができます。それをコンパクトに行うだけでは不十分な場合は、常に特定の時点で状態を記録し、以前の要求を長期ストレージにプッシュすることができます。チューニング変数の頻度。ああ、あなたはそれらの状態を不変に記録することができます。
次に、これらの新しいオブジェクトを作成するオーバーヘッドが多すぎるように見えますが、おそらく、これを無視できるようにするポインタまたは何かを使用できる低レベルのデータ構造があります。
コンストラクタが愚かなことをしない限り、これは問題ありません。属性の検証と設定に固執します。そうすることで、新しいオブジェクトを作成するコストは、ポインタを少し追加するだけでそれ以上にはなりません。テストして確認してください。
実際、GPU処理はすでに不変粒子シミュレーションの例だと思います。
これは、GPUでは、テクスチャの形で入力したデータは変更されず、テクスチャの形で新しいデータが出力されるためです。 1つのテクスチャは読み取り専用ですが、2番目のテクスチャは書き込み専用です。また、出力テクスチャが書き込まれると、変更されません。同時に、読み取りが完了すると、読み取り専用のテクスチャは古くなり、再利用できます。
したがって、次のステップでは、作成されたテクスチャが入力として使用され、読み取られたテクスチャが出力に使用されます。
しかし、これは実際には、パーティクルへのすべての参照が新しいバージョンに切り替えられた場合にのみ機能します。しかし、コードが古いデータへの参照を保持し続ける理由はわかりません。したがって、ここでの重要なポイントは、すべての参照が新しいバージョンに更新され、メモリの再利用が可能かつ効率的になるようにすることです。
可変データ構造でさえ、特にパーティクルの数に予測可能な上限がない場合のパーティクルシステムの課題の1つは、シーケンシャルアクセスでパフォーマンスが非常に重要であるというトリッキーな要件があることです(多くの場合、メモリの連続性。GPUが関係している場合は、ある程度、実際にそれを必要とします。同時に、コンテナの中央から削除すると同時に、パーティクルが発生して消滅するときにコンテナに繰り返し挿入したいとします。
インデックスの無効化がPITAである場合に、業界でこの問題を解決して一定の挿入と削除を維持するために、業界でこの問題に共通して見られる一般的な解決策の1つは、フリーリストに相当するものを使用することです。たとえば、ブロック内の64個以上のパーティクルに割り当てられたメモリを使用して、隣接するブロックにパーティクルを格納します。パーティクルのメモリチャンクが使用されていない場合は、それをリストノードとして扱い、スタックの最上部にプッシュします(これには、いくつかのポインターの操作が含まれます)。挿入時に、スタックから空きノードをポップし、そのポインタをパーティクルのメモリとして利用します(スタックが空の場合は、新しいブロックを割り当てて64以上のパーティクルを格納し、それらのチャンクを空きスタックにプッシュします)。パーティクルを削除/解放するときは、そのポインターをフリーノードとして扱い、スタックにプッシュします。これが基本的な概念であり、これよりも複雑で、空になったときにブロックを解放することができます。
この時点に達した場合、これをガベージコレクションを利用した永続的なデータ構造にしたり、共有ポインタや浅いコピーブロックへの参照などを使用した参照カウントを行ったりする作業は、それほど多くはありません。
そして、実際にこれをパーティクルシステムで試して、PDS表現を使用して400万個のパーティクルとパフォーマンスを比較しました(ただし、GIFはがらくたに見え、元のFPSをまったく伝えていません)。
そして、私が見つけたのは、元の可変構造を直接変更する構造で、約300 FPSを得たことです。フレームごとにまったく新しい構造のインスタンスを出力したPDSバージョンでは、フレームレートが120 FPSに低下しました。
ただし、シミュレーションは非常にシンプルであることを覚えておいてください。 CPUでパーティクルをラスタライズするのは簡単な作業であり、パーティクルに加えられる力は「マウスクリックに従う」という単純なものです。したがって、私は既存のデータ構造を変更することと、フレームごとに新しいバージョンを出力することの違いをストレステストするだけで、元のFPSの40%は思ったほど悪くありません。粒子の衝突検出などで粒子シミュレーションがかなり複雑になった場合、両者の比例差はかなり縮小します。
だから私は実際にそれがかなり最適化されていなかった、私が思いつくことができる最高のPDS実装でのパフォーマンスに関してはかなり合理的であるとわかりました。不変のPDSであることからどのような機能を得ることができるのかわからず、本番環境では適用していません(現在、PDSを使用してメッシュに大きなメリットをもたらしています)。もともと私は、パーティクルをこの種の分厚い、参照カウントブロックのストレステストとして使用して、パーティクル*以外の場所に適用できるかどうかを確認しようとしました。
単純なパーティクルシミュレーションでこの種のデータ構造のベンチマークを楽しむだけのように、実際にパーティクルでデータ構造を使用するつもりはありませんでした。データ構造にN回の挿入を行うのにかかる時間よりも明確ではない時間よりも、「フレームレート」の点でパフォーマンスの感覚を高めたいので、これは奇妙な古い習慣です。全体像で適用した場合のFPSへの影響について私に教えてください。それはまた、現実世界のアプリケーションに少なくとも少し似ているものの外側の構造を最適化するために、あまりにもトンネルを視野に入れないようにするための私の個人的な防御策でもあります。
ほとんどの人は、私がある程度得た正確性とスレッドセーフティについての推論の観点から、不変性の利点に焦点を当てていますが、私の場合、不変性、特に永続的なデータ構造により、すぐに見つけた多くの大きな実用的な利点はコピーして変更コピーを作成するのがどれほど安いか。元のバージョンを安くコピーして、変更された関連部分のみを含む新しい変更されたコピーを作成し、深いコピーを必要とするので交換できるため、元に戻す、例外安全、非破壊編集などの実装は簡単です。元に戻す/ロールバック/非破壊的な再評価に似たものについては、古いバージョンで戻します。
非常に動的な性質を考えると、パーティクルの難しさは、おそらくデータ構造内のすべてのパーティクルがすべてのフレームに触れられることになるため、いずれにせよ、すべてのフレームをすべてのフレームに深くコピーすることになります。この場合、1つのPDSに完全なパーティクルを格納するのではなく、パーティクルの位置と速度のみに専用のフィールド、パーティクルサイズや寿命などのより低温のフィールドに別のフィールドを格納する並列フィールドをこのように並べると便利です。 。このように、フレームごとに出力する必要があるデータは、前のものとは異なり、少し縮小する可能性があります。
1つの使用例
私は上記の不変の表現の実際の使用例を考えようとしていました(これは実際にはパーティクルに使用するつもりはありませんでした。パーティクルを使用してすばやくテストしただけです)。 「非常に動的」な要件を緩和することは、粒子ペイントソフトウェアのようなものです。
ユーザーが実際のメディアをシミュレートするように設計されたキャンバス上に粒子をスプレーして散布するように、ピクセルやベクトルを操作する代わりに、実際には速度のある数百万から数百万の粒子のボートロードを操作しています。そして、しばらくすると粒子は動きを止めて落ち着きますが、ユーザーはツールをつかんで塗抹し、ペイント粒子をあちこちに塗りつけ始めるかもしれません。そして、ユーザーがズームインして、パーティクルをいくつかいじるだけの場合もあります。
その場合、それはかなり動的ですがevery単一のパーティクルがすべての単一のフレームを移動していないため、不変の表現の一種は、ユーザーの取り消しを次のように保存するかなり効率的な方法です。ユーザーの元に戻す操作がキャプチャされた時点から生成されたPDSの新しいバージョンで実際に移動したパーティクルに必要なメモリの大部分のみが必要になるため、メモリ使用量を最小限に抑えます。
そして、パーティクルには、いくらかの奥行きとシェーディング/ライティングがあり、いくつかのインパストエフェクトを可能にします。
それは一種の楽しいアイデアです。楽しい小さなペイントソフトウェアのためだけに、週末にコードを書くかもしれません。