まず、必ずしもデータ指向の設計をあらゆる場所に適用する必要はありません。これは究極的には最適化であり、パフォーマンスが重要なコードベースでさえも、その恩恵を受けない部分がたくさんあります。
私はしばしば、それを、より効率的に処理できる大きなデータ ブロックを優先して、構造を消去するものと考える傾向があります。たとえば、画像を見てください。ピクセルを効率的に表現するには、一般に、単純な数値の配列を格納する必要があります。たとえば、誇張された例として仮想ポインタを持つユーザー定義の抽象的なピクセル オブジェクトのコレクションではありません。
float を使用する 4 コンポーネント (RGBA) の 32 ビット イメージを想像してみてください。ピクセル型にベーシックstruct
を使用したとしても、通常は、配置に必要な構造体のパディングにより、ピクセル構造体を使用してかなり多くのメモリを必要とすることになります。
struct Image
{
struct Pixel
{
float r;
float g;
float b;
unsigned char alpha;
// some padding (3 bytes, e.g., assuming 32-bit alignment
// for floats and 8-bit alignment for unsigned char)
};
vector<Pixel> Pixels;
};
この単純なケースでも、8 ビット アルファの並列配列を持つ float のフラット配列に変換すると、メモリ サイズが削減され、結果としてシーケンシャル アクセス速度が向上する可能性があります。
struct Image
{
vector<float> rgb;
vector<unsigned char> alpha;
};
...そして、それが私たちが最初に考えるべき方法です:データ、メモリレイアウトについて。もちろん、画像はすでに一般的に効率的に表現されており、画像処理アルゴリズムはすでに実装されており、多数のピクセルをまとめて処理しています。
しかし、データ指向の設計では、この種の表現をピクセルよりもかなり高いレベルのものにも適用することで、これを通常よりもさらに高いレベルに引き上げます。同様ParticleSystem
に、単一の の代わりに をモデリングしてParticle
、最適化のための余裕を残すこと、またはのPeople
代わりに をモデリングすることでメリットが得られる場合がありPerson
ます。
しかし、画像の例に戻りましょう。これは、DOD の欠如を意味する傾向があります。
struct Image
{
struct Pixel
{
// Adjust the brightness of this pixel.
void adjust_brightness(float amount);
float r;
float g;
float b;
};
vector<Pixel> Pixels;
};
このadjust_brightness
方法の問題点は、インターフェイスの観点から、単一のピクセルで動作するように設計されていることです。これにより、一度に複数のピクセルにアクセスできることから恩恵を受ける最適化とアルゴリズムを適用することが難しくなる可能性があります。一方、次のようなもの:
struct Image
{
vector<float> rgb;
};
void adjust_brightness(Image& img, float amount);
...一度に複数のピクセルにアクセスすることで恩恵を受ける方法で書くことができます。SoA 担当者を使用して、次のように表すこともできます。
struct Image
{
vector<float> r;
vector<float> g;
vector<float> b;
};
...ホットスポットが順次処理に関連している場合、これは最適かもしれません。詳細はそれほど重要ではありません。私にとって重要なことは、設計に最適化の余地を残しておくことです。私にとって国防総省の価値は、実際に、そのような考えを前もって考えることで、これらのタイプのインターフェース設計が得られ、邪魔な設計変更をせずに、必要に応じて後で最適化する余地が残されていることです。
ポリモーフィズム
Dog
ポリモーフィズムの古典的な例は、継承のように、一度に 1 つずつという細かな考え方にも焦点を当てる傾向がありますMammal
。開発者が型システムと戦わなければならないボトルネックにつながる可能性のあるゲームでは、ポリモーフィック ベース ポインターをサブタイプ別に並べ替えて、vtable の一時的な局所性を改善し、データを特定のサブタイプ (Dog
など) にカスタム アロケーターで連続して割り当てようとします。各サブタイプ インスタンスの空間的局所性を改善するなど。
より粗いレベルでモデル化する場合、これらの負担は必要ありません。Dogs
abstractを継承できますMammals
。現在、仮想ディスパッチのコストは、哺乳動物ごとに 1 回ではなく、哺乳動物の種類ごとに 1 回に削減され、特定の種類のすべての哺乳動物を効率的かつ連続的に表すことができます。
DOD の考え方で OOP とポリモーフィズムを利用することもできます。秘訣は、型システムと戦ったり、メモリ レイアウトなどの制御を取り戻すためにデータ型を回避したりしないように、十分に粗いレベルで物事を設計していることを確認することです。十分に粗いレベルで設計する場合は、そのようなことを気にする必要はありません。
インターフェイス設計
少なくとも私が見る限り、DOD に関連するインターフェース設計はまだ残っており、クラスにメソッドを含めることができます。適切な高レベル インターフェイスを設計することは依然として非常に重要であり、仮想関数とテンプレートを使用して非常に抽象的にすることもできます。私が焦点を当てる実際的な違いは、上記の方法の場合のように、集約インターフェイスadjust_brightness
を設計することです。これにより、コードベース全体に設計変更をカスケードすることなく、最適化する余地が残されます。一度に 1 つのピクセルを処理するインターフェイスではなく、画像全体の複数のピクセルを処理するインターフェイスを設計します。
DOD インターフェイスの設計は、多くの場合、大量に処理するように設計されており、通常は、すべてにアクセスする必要がある最もパフォーマンスが重要で、線形で複雑なシーケンシャル ループに最適なメモリ レイアウトを持つ方法で設計されています。
したがって、あなたの例を で見るとModel
、欠けているのはインターフェースの集約側です。
struct Models {
// Methods to process models in bulk can go here.
struct Model {
// vertex buffers
GLuint Positions, Normals, Texcoords, Elements;
// textures
GLuint Diffuse, Normal, Specular;
// further material properties
GLfloat Shininess;
};
std::vector<Model> models;
};
これは、メソッドを持つクラスを使用して厳密に表現する必要はありません。の配列を受け入れる関数である可能性がありますstructs
。これらの詳細はそれほど重要ではありません。重要なのは、インターフェイスがほとんどの場合、一括して順次処理するように設計されているのに対し、データ表現はその場合に最適に設計されていることです。
ホット/コールドスプリット
クラスを見ると、Person
まだ古典的なインターフェースのような方法で考えているかもしれません (ここでのインターフェースは単なるデータですが)。繰り返しますが、DOD は主にstruct
、パフォーマンスが最も重要なループに最適なメモリ構成である場合にのみ、全体に a を使用します。人間にとっての論理的な組織ではなく、機械にとってのデータの組織です。
struct Person {
Person() : Walking(false), Jumping(false) {}
float Height, Mass;
bool Walking, Jumping;
};
まず、これを文脈に入れましょう:
struct People {
struct Person {
Person() : Walking(false), Jumping(false) {}
float Height, Mass;
bool Walking, Jumping;
};
};
この場合、すべてのフィールドが一緒にアクセスされることがよくありますか? 仮に、答えがノーだとしましょう。これらのWalking
およびJumping
フィールドは時々しかアクセスされず (コールド)、Height
およびMass
は常に繰り返しアクセスされます (ホット)。この場合、潜在的により最適な表現は次のようになります。
struct People {
vector<float> HeightMass;
vector<bool> WalkingJumping;
};
もちろん、ここで 2 つの別個の構造体を作成したり、一方を他方に向けたりすることもできます。重要なのは、メモリ レイアウト/パフォーマンスの観点から最終的にこれを設計することです。一般的なユーザー エンド コード パス。
インターフェイスの観点からは、人ではなく人を処理することに重点を置いてインターフェイスを設計します。
問題
それが邪魔にならないように、あなたの問題に進みます:
このモジュールからのみモデルを作成でき、他のモジュールからは作成できません。これを複雑化しながら型クラス Model に移動する必要がありますか?
これは、サブシステムの設計上の懸念事項です。あなたのModel
担当者はすべてOpenGLデータに関するものなので、適切に初期化/破棄/レンダリングできるモジュールに属しているはずです。このモジュールの非公開/非表示の実装の詳細である場合もあり、その時点で、モジュールの実装内に DOD の考え方を適用します。
ただし、モデルの追加、モデルの破棄、レンダリングなどのために外の世界で使用できるインターフェイスは、最終的には一括処理用に設計する必要があります。上記の画像の例のように、各要素に追加したくなるようなメソッドが最終的にコンテナーに属してしまう、コンテナーの高レベル インターフェイスを設計することと考えてくださいadjust_brightness
。
複雑な初期化/破棄には、一度に 1 つずつ設計する考え方が必要になることがよくありますが、重要なのは、集約インターフェイスを介してこれを行うことです。Model
ここでは、レンダリングする GPU を追加する際に初期化を行いModel
、リストから GPU を削除する際に GPU リソースをクリーンアップすることを優先して、標準のコンストラクターとデストラクタを放棄することができます。個々のタイプ (person など) の C スタイルのコーディングにやや戻っていますが、集約インターフェイス (people など) の C++ グッズを使用して非常に洗練されたものにすることができます。
私の質問は、型クラスにメソッドを追加する必要があるかどうかです。
主にバルク用に設計されており、途中である必要があります。あなたが示した例では、通常はいいえ。最も難しいルールである必要はありませんが、型は個々のものをモデル化しており、DOD の余地を残すには、ズームアウトして、多くのものを処理するインターフェイスを設計する必要があります。