私のC++プロジェクトには、Particle
、Contact
、およびNetwork
の3つのクラスがあります。 Network
クラスには、N個のパーティクル(std::vector<Particle> particles
)とNc個の連絡先(std::vector<Contact> contacts
)があります。 Particle
オブジェクトにはそれぞれ、Contact
オブジェクトで表される多数の連絡先が含まれ、連絡先はパーティクルのペアによって共有されます。
特定の粒子の接触を知る必要があるため、Network
オブジェクトを通じて(たとえば、network.getContacts(Particle particle)
を使用して)その情報を取得できるようにすることにしました。
私の質問は2つの部分に分けられます。
最初、各パーティクルの連絡先を保存するための効率的でコミュニティ推奨の方法は何ですか?
Contact
オブジェクトへの共有ポインタのベクトルのベクトル、vector<vector<shared_ptr<Contact>>> prtContacts
(見やすくするためにstd::
を省略している)を作成できるので、prtContacts[i]
に連絡先への共有ポインタのベクトルが含まれると思いますi
番目の粒子の。このコンテキストでshared_ptr
を使用すると、共有の連絡先を持つ2つのパーティクルに対して重複するContact
オブジェクトを作成しないことが保証されるため、これが役立つと示唆されています。より意味のある代替方法はありますか?
2番目、上で定義したようにprtContacts
を作成してしまう場合、それを初期化する最良の方法は何ですか?私の現在の考えは、Ncの連絡先でstd::vector<Contact> contacts
を初期化する最初のループを作成し、次に連絡先への共有ポインターでprtContacts
を初期化する2番目のループを作成することです。それは妥当ですか、それとも、代替のより効率的なアプローチはありますか?
ありがとうございました!
いくつかの追加情報:
パーティクルの数Nは、O(10^1)
からO(10^4)
までの範囲になります。粒子あたりの接触数は、おそらく6〜7を超えませんが、変化することが許容されます。各接触には、位置と接触力(2成分ベクトル)が関連付けられており、シミュレーション(モンテカルロシミュレーションなど)の過程で位置と接触力の両方を変化させることができます。
適切なソリューションは、機能以外の要件に大きく依存します。通常、いくつのパーティクルを使用しますか?パーティクルには通常いくつの接触がありますか?ネットワークはどの程度更新され、どのように更新されますか?それはどれくらい速くする必要がありますか?
可能であれば、インデックスでオブジェクトを参照することをお勧めします。パーティクルの連絡先は、連絡先のインデックスのリストになります。通常、パーティクルの接点数が少ない場合は、vector
の代わりに、boost::container::small_vector
、absl::inlined_vector
、folly::small_vector
、llvm::SmallVector
などの少数の要素用に最適化されたものを使用することをお勧めします。
using ParticleContacts = boost::container::small_vector<int, 12>;
class Network {
std::vector<Particle> particles;
std::vector<Contact> contacts;
std::vector<ParticleContacts> particle_contacts;
public:
ParticleContacts getParticleContacts(int particle_index) const {
return particle_contacts.at(particle_index);
}
Contact getContact(int contact_index) const {
return contacts.at(contact_index);
}
//...
};
ライブデモ 。
編集:連絡先のvector
の途中から削除できるようにする必要がある場合、インデックスの使用は機能しません。これが不要になるようにアルゴリズムを変更できるかどうかを真剣に検討します。ポインタ、イテレータ、インデックスを無効にするだけでなく、削除後のすべての連絡先を移動する必要があるため、非常に非効率的です。たとえば、パーティクルに接続されていないシミュレーションが終了するまで、孤立した連絡先を残すことができます。とにかく、本当に連絡先のリストの中央から削除できるようにする必要がある場合は、おそらくvector
は最適なデータ構造ではなく、リンクされたリストなどを好むはずです。リンクリストの利点は、途中からすばやく削除できることと、リスト内の連絡先へのポインターが挿入または削除によって無効にされないため、パーティクルの連絡先のリストを生のポインターにすることができることです。欠点は、速度がはるかに遅く、ランダムアクセスが許可されないことです。
using ParticleContacts = boost::container::small_vector<Contact*, 12>;
class Network {
std::vector<Particle> particles;
std::forward_list<Contact> contacts;
std::vector<ParticleContacts> particle_contacts;
public:
ParticleContacts getParticleContacts(int particle_index) const {
return particle_contacts.at(particle_index);
}
void addParticle(const Particle& particle) {
particles.Push_back(particle);
particle_contacts.emplace_back();
}
Contact* addParticleContact(int particle_index_1, int particle_index_2) {
contacts.emplace_front();
particle_contacts.at(particle_index_1).Push_back(&contacts.front());
particle_contacts.at(particle_index_2).Push_back(&contacts.front());
return &contacts.front();
}
};
ライブデモ 。
または、std::vector<std::unique_ptr<Contact>>
を使用することもできます。連絡先への生のポインタは、挿入や削除によって無効化されません。ランダムアクセスを許可するという利点がありますが、挿入と削除が遅いという欠点があります。 ライブデモ
正しい選択は、データがシミュレーションによって通常どのようにアクセスされるかに依存します。
したがって、これは実際に予想されるアクセスパターンに依存します。まず、shared_ptr
は、マルチスレッドプログラムで大きなコストがかかる可能性があります。参照カウントをインクリメントおよびデクリメントすると、他のCPUでプロセッサキャッシュが無効になります。これにより、これらの操作のコストが少し高くなります。また、参照カウントに近いバイトへの読み取りアクセスであってもメインメモリが非常に高価になるため、これらのオブジェクトが複数のスレッドから何らかの方法でアクセスされている場合は、大量のキャッシュスラッシングが発生します。参照カウントが更新された後にフェッチします。
これを軽減する1つの方法は、言われているようにmake_shared
を使用しないことです。これにより、参照カウントがオブジェクトとは別に割り当てられます。しかし、それによってキャッシュの問題が別の場所に移動する可能性があります。
さまざまな種類のポインタは、所有権を表現することです。オブジェクト本当には所有権を共有していますか?それとも、ネットワーク全体をオブジェクトを所有していると考える方が適切ですか?後者の場合は、ベアポインターを格納し、すべてのグラフ要素に対するunique_ptr
sを保持するいくつかのvector
オブジェクト(タイプごとに1つ)をネットワークに配置するほうが理にかなっています。
パーティクルあたりの接触数は、適度に小さい数に制限されていますか?その場合、固定サイズarray
を使用して連絡先へのベアポインターのリストを格納し、実際にすべてのオブジェクトを所有するNetworkオブジェクトを使用することは理にかなっています。
私はもっと考慮すべき事があると確信しています。そして、ここでの私の主な焦点は、物事を行う「正しい」方法の抽象的な概念ではなく、パフォーマンスにありました。おそらく、パフォーマンス以外の何かが主な目標です。しかし、その場合、なぜここでC++を使用するのですか?
私は主にクリスの答えをエコーしますが、いくつかの可能な逸脱があります。私が心からこだわる究極のことは、いくつかの大きなベクトルpersネットワークのような大きなシーケンスに格納された、パーティクルとコンタクトへのインデックスを格納することです。
ただし、私が同意しない部分は、あなたがインデックスとランダムアクセスシーケンスを使用でき、それでも一定の時間の削除(非常に小さな定数で)が可能であり、インデックス付きのフリーリスト/ホール戦略を使用します。パーティクルが消滅した場合、次のように、余分なメモリを使用せずに、一定の時間でパーティクルを削除し、一定の時間で再利用/挿入できます。
... _union Nodes nodes[n]
_は_std::vector<union Node>
_になる可能性があります。 Particle
またはContact
が自明に構築可能または破壊可能でない場合は、少しエルボグリースが必要です(非POD用のC++で適切なコンテナーを作成することは、新しい配置のPITAのビットです)ストレージ、および手動のdtor呼び出し)。うまくいけば、これらのデータは、一般にこのタイプのデータに当てはまる可能性があります。
最初に、各パーティクルの連絡先を保存するための効率的でコミュニティ推奨の方法は何でしょうか。
すべてのパーティクルの接触を一度にシミュレーションして収集することが効率的であると仮定すると(多くのシミュレーションでよくあることです)、短い答え:
_// Stores all the contacts between particles. Each per-particle
// contact list begins with a count followed by the indices to each
// contact.
std::vector<int> contact_data;
// Stores an index into the contact data for each particle.
std::vector<int> contacts;
_
N番目のパーティクルの接触を取得するには、次のようにします。
_// Fetch the index into the contact data for the nth particle.
const int cd_index = contacts[n];
// Fetch the number of contacts.
const int num = contact_data[cd_index];
// Fetch a pointer to the array of contact indices.
const int* indices = contact_data.data() + cd_index + 1;
_
これで、n番目のパーティクルの接触のインデックスとその数がわかりました。リストをクリアして、タイムステップごとに計算できます。少しの作業でマルチスレッド化することもできます(各スレッドの粒子の範囲の接触をローカルに収集し、最後に結果を2つのベクトルにマージします)。
network.getContacts(Particle particle)
提案に従ってインデックスを使用すると、上記は次のようになります。
_// Returns the number of contacts and a pointer to the array
// of contact indices for the particle indexed by 'particle_index'.
std::pair<int, const int*> network.getContacts(int particle_index)
_
...独自の構造体やクラスなど、_std::pair
_よりも少し良いものを使用できます。
SmallVectorsおよび同類
_boost::small_vector
_または_llvm::SmallVector
_のようなものを使用することは非常に優れたソリューションであり、それぞれに_std::vector
_を使用した場合のように、すべてのパーティクルにヒープ割り当てを要求するよりもはるかに優れています。クリスの答えはすでに本当に良いです。ただし、連絡先を永続的に保存する場合は、これらの小さなバッファーの最適化により、メモリ使用量が少し爆発し始め、余分なキャッシュミスにつながる可能性があります(スタック上での短期間のストレージにはかなり優れています)。 getContacts
でオンデマンドで計算するよりも、一度にすべての連絡先を計算する方がシミュレーションの方が良いと想定して、整数の大きな古いベクトルだけを使用することをお勧めします(この時点で、小さなベクトルははるかに適切です)。
[...]これは、共有の連絡先を持つ2つのパーティクルに対して重複した連絡先オブジェクトを作成しないことを保証するため、このコンテキストでは便利です。より意味のある代替方法はありますか?
_shared_ptr
_は、リソースの所有権を共有し、すべての所有者が所有権を解放するまでその寿命を延ばすためのものです。所有権を共有せずに(ここで必要なのはこれだけです)、リソースのデータを複製せずに複数の場所でリソースを参照するには、単純な古いインデックスまたはポインターを使用します。パーティクル単位またはコンタクト単位で使用される_shared_ptr
_には、かなり大きな相対オーバーヘッドがあります。
上記の手法(インデックス付きフリーリストを含む)を使用して実際に迅速に行うこと、およびこの場合は実際には「パーティクル」ではなく、エージェントとエージェント間の衝突リスト(同時に2つの整数のベクトルに収集されたエージェント)上に示したように):500,000のエージェントがお互いに跳ね返り、シングルスレッドで、単にシミュレーションするだけでなく、単にピクセルをプロットするのに費やされた時間のかなりの部分で:
最初に、各パーティクルの連絡先を保存するための効率的でコミュニティ推奨の方法は何でしょうか。
大量の小さなリストを格納するという誘惑に非常に効率的に対応したい場合は、すべてのデータを小さなものではなく大きなシーケンスで格納するのが理想的です。 100万個の要素に関連付けられた100万個のコンテナに相当するものを表すために、実際には100万個のコンテナをインスタンス化する必要はありません。たとえば、2つだけを使用できます。1つはすべてのデータを格納し、もう1つはそのデータへの開始インデックスを格納する100万個の要素に対応しています。
これは、コンテキストに関係なく一般的な最適化戦略であり、多くの便利なコンテナを提供する言語での一般的なパフォーマンスの問題に対処します。これらのコンテナーは、1つのコンテナーに100万個の要素を格納するには非常に効率的ですが、それぞれに数個の要素を持つ100万個のコンテナーを格納するにはあまり効率的ではありません。これは、SmallVector
や_std::string
_のようなSBO(小さなバッファーの最適化)を使用するものにも当てはまります。これは、「小さなバッファー」が大きすぎる(この時点で、メモリを浪費して巨大になる)ためです。ある要素から次の要素にストライド)、または小さすぎてヒープ割り当てが発生します。すべてのデータに1つの大きなバッファを使用する場合のように、通常は「適切」ではありません。
この答えは少しやり過ぎかもしれません。私は実際には生産性をまったく考慮しておらず、すべてを1つの大きなコンテナーに格納し、おそらく上記のインデックス付きの空きリストを実装するという不便に対処するために少し時間があると仮定します(私は頼むのに適した人ではありません) 10分で実装できる効率的なソリューション...まあ、私はこれらを10分で実装できると思います。 VFXの場合、数億個のパーティクルを処理することは珍しくありません。そのため、他のいくつかのソフトウェアにユーザーが文句を言わないように、許容できるパフォーマンスを得るために、これらのタイプのテクニックとハンドロールコンテナーを最低限使用する必要があることがよくあります。より高速であるか、スタジオでファームの別のものを使用してシミュレーションに切り替えます。しかし、物事をスピードアップするためのいくつかの非常に一般的なテクニックを共有することに少し興奮します。あまり豪華にならず、できれば過剰すぎない優れたテクニックは、要素にインデックスを付けてすべてを大きなコンテナに格納するだけで、小さなもの。最大のスケール入力用のインデックス付きフリーリストの代替を含む(実装するのがはるかに複雑)ため、このトピックでカバーできることがたくさんあるので、私は実際にこの答えについて多くを抑制していますが、ここで止めます!
私は尋ねなければなりません-Contact
sがParticle
sと分離しているのはなぜですか?各Particle
には連絡先のリストが含まれているようです。または、それが適切でない場合は、class
およびstruct
の配列(またはParticle
など)を含む別のvector
またはContact
_s。何かのようなもの:
struct ParticleCollision {
Particle part;
std::vector<Contact> contacts;
};
その場合、Network
にはParticleCollision
sのstd::vector
が含まれます。