コードベース(ECSエンジン)の最も中心的な部分のいくつかを、記述したデータ構造のタイプを中心に展開しますが、より小さい連続したブロック(4メガバイトではなく4キロバイトなど)を使用します。

ダブルフリーリストを使用して、挿入する準備ができているフリーブロック(完全ではないブロック)の1つのフリーリストと、そのブロック内のインデックスのブロック内のサブフリーリストで、一定時間の挿入と削除を実現します。挿入時に再利用する準備ができています。
この構造の長所と短所について説明します。いくつかの短所があるので、いくつかの短所から始めましょう。
短所
- この構造に数億個の要素を挿入するには
std::vector
、純粋に連続した構造よりも約4倍時間がかかります。そして、私はマイクロ最適化にかなりまともですが、一般的なケースでは最初にブロックの空きリストの上部にある空きブロックを検査し、次にブロックにアクセスしてブロックの空きインデックスをポップする必要があるため、概念的にはやるべきことがあります空きリストで、要素を空き位置に書き込み、ブロックがいっぱいかどうかを確認し、いっぱいであればブロック空きリストからブロックをポップします。まだ一定時間の操作ですが、に戻るよりもはるかに大きな定数を使用していstd::vector
ます。
- インデックス作成のための追加の算術演算と間接的な追加のレイヤーを考慮して、ランダムアクセスパターンを使用して要素にアクセスする場合、約2倍の時間がかかります。
- イテレータはインクリメントされるたびに追加の分岐を実行する必要があるため、シーケンシャルアクセスはイテレータデザインに効率的にマッピングされません。
- 通常、要素ごとに約1ビットのメモリオーバーヘッドが少しあります。要素ごとに1ビットはあまり聞こえないかもしれませんが、これを使用して100万個の16ビット整数を格納すると、完全にコンパクトな配列よりも6.25%多くのメモリが使用されます。ただし、実際には、これ
std::vector
を圧縮しvector
て予約する余分な容量を排除しない限り、使用するメモリが少なくなる傾向があります。また、私は通常、そのような小さな要素を保存するためにそれを使用しません。
長所
for_each
ブロック内の要素の範囲を処理するコールバックを使用する関数を使用したシーケンシャルアクセスは、シーケンシャルアクセスの速度にほぼ匹敵しますstd::vector
(10%の差分のみ)。 ECSエンジンで費やされる時間のほとんどはシーケンシャルアクセスです。
- ブロックが完全に空になったときにブロックの割り当てを解除する構造により、中間からの一定時間の削除が可能です。その結果、通常、データ構造が必要以上に大量のメモリを使用しないようにすることは非常に適切です。
- コンテナから直接削除されない要素のインデックスは無効になりません。これは、フリーリストアプローチを使用して、後続の挿入時にそれらの穴を取り戻すために穴を残すだけなので、コンテナから直接削除されません。
- この構造が膨大な数の要素を保持している場合でも、OSが膨大な数の連続した未使用を見つけるために挑戦しない小さな連続ブロックのみを要求するため、メモリ不足を心配する必要はありません。ページ。
- 操作は一般に個々のブロックにローカライズされるため、構造全体をロックすることなく、並行性とスレッドセーフに適しています。
私にとって最大の長所の1つは、次のように、このデータ構造の不変バージョンを作成するのが簡単になることです。

それ以来、例外の安全性、スレッドの安全性などを達成するのをはるかに容易にする副作用のないより多くの関数を書くためのあらゆる種類の扉を開いた。このデータ構造は後から見たものであり、偶然ですが、間違いなくコードベースの保守がはるかに簡単になったため、最終的に得られた最も素晴らしい利点の1つです。
不連続な配列にはキャッシュの局所性がないため、パフォーマンスが低下します。ただし、4Mのブロックサイズでは、適切なキャッシングに十分なローカリティがあるようです。
参照の局所性は、4キロバイトのブロックは言うまでもなく、そのサイズのブロックで心配することではありません。通常、キャッシュラインはわずか64バイトです。キャッシュミスを減らしたい場合は、それらのブロックを適切に配置することに集中し、可能な場合はより多くのシーケンシャルアクセスパターンを優先します。
ランダムアクセスメモリパターンをシーケンシャルパターンに変換する非常に迅速な方法は、ビットセットを使用することです。大量のインデックスがあり、それらがランダムな順序で並んでいるとしましょう。それらをただ耕し、ビットセットのビットをマークすることができます。次に、ビットセットを反復処理して、一度に64ビットなどの非ゼロのバイトを確認できます。少なくとも1つのビットが設定されている64ビットのセットに遭遇したら、FFS命令を使用して、設定されているビットをすばやく判断できます。このビットは、アクセスするインデックスを示しますが、現在はインデックスを順番に並べ替えます。
これにはいくらかのオーバーヘッドがありますが、特にこれらのインデックスを何度もループする場合には、場合によってはやりがいのある交換になります。
アイテムへのアクセスはそれほど単純ではなく、間接的なレベルが余分にあります。これは最適化されますか?キャッシュの問題が発生しますか?
いいえ、最適化して削除することはできません。少なくとも、この構造ではランダムアクセスのコストが高くなります。特に一般的なケースの実行パスがシーケンシャルアクセスパターンを使用している場合は、ブロックへのポインターの配列で一時的な局所性が高くなる傾向があるため、キャッシュミスはそれほど増加しません。
4Mの制限に達すると線形に増加するため、通常よりも多くの割り当てを行うことができます(たとえば、1 GBのメモリに対して最大250の割り当て)。4M以降は追加のメモリはコピーされませんが、追加の割り当てがメモリの大きなチャンクをコピーするよりも高価かどうかはわかりません。
実際には、コピーはまれなケースであるため、コピーはより高速であることが多く、log(N)/log(2)
合計時間のようなものが発生するだけで、同時に、要素がいっぱいになり再配置する必要がある前に何度も要素を書き込むことができる一般的なケースを簡素化します。したがって、通常、このタイプの構造では、巨大な配列を再割り当てする高価なまれなケースを処理する必要がない場合でも、一般的なケースの作業はより高価になるため、挿入が速くなりません。
すべての短所にもかかわらず、この構造の主な魅力は、メモリ使用量の削減であり、OOMを心配する必要がなく、無効化されないインデックスとポインタを保存できること、同時実行性、および不変性です。構造体へのポインタとインデックスを無効にせずに自分自身をクリーンアップしながら、一定時間内に物を挿入および削除できるデータ構造があると便利です。