1

オブジェクト指向プログラミング (OOP) ソリューションとボールの束を更新するデータ指向設計 (DOD) ソリューションを比較するサンプル コードを次に示します。

const size_t ArraySize = 1000;

class Ball
{
public:
    float x,y,z;
    Ball():
        x(0),
        y(0),
        z(0)
    {
    }

    void Update()
    {
        x += 5;
        y += 5;
        z += 5;
    }
};

std::vector<Ball> g_balls(ArraySize);

class Balls
{
public:
    std::vector<float> x;
    std::vector<float> y;
    std::vector<float> z;

    Balls():
        x(ArraySize,0),
        y(ArraySize,0),
        z(ArraySize,0)
    {
    }

    void Update()
    {
        const size_t num = x.size();
        if(num == 0)
        {
            return;
        }

        const float* lastX = &x[num - 1];

        float* pX = &x[0];
        float* pY = &y[0];
        float* pZ = &z[0];
        for( ; pX <= lastX; ++pX, ++pY, ++pZ)
        {
            *pX += 5;
            *pY += 5;
            *pZ += 5;
        }
    }
};

int main()
{
    Balls balls;

    Timer time1;
    time1.Start();
    balls.Update();
    time1.Stop();

    Timer time2;
    time2.Start();
    const size_t arrSize = g_balls.size();
    if(arrSize > 0)
    {
        const Ball* lastBall = &g_balls[arrSize - 1];
        Ball* pBall = &g_balls[0];
        for( ; pBall <= lastBall; ++pBall)
        {
            pBall->Update();
        }
    }
    time2.Stop();


    printf("Data Oriented design time: %f\n",time1.Get_Microseconds());
    printf("OOB oriented design  time: %f\n",time2.Get_Microseconds());

    return 0;
}

さて、これはVisual Studioでコンパイルして実行しますが、これを行うことが許可されているかどうか疑問に思っていますが、これを確実に行うことができるはずです:

const float* lastX = &x[num - 1];//remember, x is a std::vector of floats

float* pX = &x[0];//remember, x is a std::vector of floats
float* pY = &y[0];//remember, y is a std::vector of floats
float* pZ = &z[0];//remember, z is a std::vector of floats
for( ; pX <= lastX; ++pX, ++pY, ++pZ)
{
    *pX += 5;
    *pY += 5;
    *pZ += 5;
}

私の理解では、 std::vector のデータは連続しているはずですが、これが別のプラットフォームで問題になるかどうか、標準に違反するかどうか、内部でどのように保存されているかはわかりません。また、これは私が DOD ソリューションを OOP ソリューションよりも優れたものにすることができた唯一の方法であり、反復の他の方法はそれほど良くありませんでした。イテレータを使用することもできますが、最適化が有効になっている、別名リリース モードの OOP よりも高速であると確信しています。

それで、これは DOD を行う良い方法ですか (最善の方法ですか?)、これは正当な c++ ですか?

[編集] さて、国防総省の場合、これは悪い例です。x、y、z は Vector3 にパッケージ化する必要があります。したがって、DOD は OOP よりもデバッグで高速に実行されましたが、リリースでは別の話でした。繰り返しますが、これは DOD を効率的に使用する方法の悪い例ですが、同時に多数のデータにアクセスする必要がある場合は短所であることを示しています。DODを使いこなすコツは、「アクセスパターンに基づいたデータ設計」です。

4

3 に答える 3

4

すべてのコードなどに関する質問は少し複雑なので、本当に必要なものを理解しているかどうかを確認してみましょう。

私の理解から、 std::vector のデータは連続しているはずです

です。標準では、ベクトル内のデータが連続して格納されることが義務付けられています。つまり、これは、標準に準拠するすべてのプラットフォーム/コンパイラに当てはまります。

これが、DOD ソリューションを OOP ソリューションよりも優れたものにすることができた唯一の方法でした

DODの意味がわからん

イテレータを使用できますが、最適化によってのみ高速になる可能性があると確信しています

実際、この場合の反復子 (VS でデバッグ反復子が無効になっていると仮定) は、ポインターによる直接変更よりも高速ではないにしても、同じくらい高速です。ベクターへの反復子は、要素へのプレーンなポインターで実装できます。繰り返しますが、デフォルトでは、VS イテレータはデバッグを支援するために余分な作業を行うことに注意してください。

次に考慮すべきことは、2 つのアプローチのメモリ レイアウトが異なることです。つまり、後の段階で all にアクセスする必要がありxy単一zの要素からアクセスする必要がある場合、最初のケースでは、おそらく単一のキャッシュ ラインに分類されます。 、3 つのベクトルのアプローチでは、3 つの異なる場所からメモリを取得する必要があります。

于 2012-02-15T18:05:31.673 に答える
1

はい、あなたはこれを行うことができます。

ベクトルコンテナは動的配列として実装されます。通常の配列と同様に、ベクターコンテナの要素は連続した保存場所に保存されます。つまり、イテレータを使用するだけでなく、要素への通常のポインタのオフセットを使用して要素にアクセスできます。 http://cplusplus.com/reference/stl/vector/

于 2012-02-15T18:04:13.413 に答える
1

既に指摘したように、vector は一般に C++11 より前は連続していましたが、現在は vector がdata使用する内部配列への直接ポインタを実際に返す新しいメソッドでそのように保証されています。ISO C++ 標準の引用は次のとおりです。

23.2.6 クラス テンプレート ベクター [ベクター]

[...] ベクトルの要素は連続して格納されます。つまり、v が T 以外の型のベクトルである場合、すべてboolの同一性に従います。&v[n] == &v[0] + n0 <= n < v.size()

そうは言っても、主にあなたがベンチマークを行って「DOD」を使用している方法のために、私は飛び込みたかったのです。

したがって、DOD は OOP よりもデバッグで高速に実行されましたが、リリースでは別の話でした。

DOD はすべてに SoA を使用することと同義ではないため、特にパフォーマンスの低下につながる場合は、この種の文はあまり意味がありません。

データ指向の設計は、データを格納して効率的にアクセスする方法を事前に検討する一般化された設計アプローチです。この考え方を使ってデザインにアプローチするとき、最初に考慮すべきことの 1 つになります。反対に、アーキテクチャの設計から始めて、オブジェクト、抽象化、純粋なインターフェイスなどとともに提供する必要があるすべての機能を理解しようとし、後でデータを実装の詳細として残します。国防総省は、設計段階で考慮すべき基本的な事柄としてデータから始め、後付けとして記入する実装の詳細ではありません。これは、パフォーマンスが顧客が要求する基本的な設計レベルの要件であり、単なる実装上の贅沢ではなく、パフォーマンスが重要な場合に役立ちます。

場合によっては、データ構造の効率的な表現が実際に新しい機能につながり、データ自体がソフトウェアを設計できるようになります。Git はそのようなソフトウェアの例であり、その機能は実際には変更セットのデータ構造をある程度中心に展開しており、その効率が実際に新しい機能の考案につながっています。そのような場合、ソフトウェアの機能とユーザーエンドの設計は、実際にはその効率性から進化し、新しい扉が開かれます。効率性により、以前は計算コストがかかりすぎて合理的な量で実行できないと考えられていたことがインタラクティブに実行できるようになるためです。時間。もう 1 つの例は、数十年前には人々が不可能だと思っていたことを可能にすることで、私の VFX 業界を再構築した ZBrush です。たとえば、スカルプト ブラシを使用して 2,000 万個のポリゴン メッシュをインタラクティブにスカルプトし、90 年代後半から 2000 年代前半には誰も見たことのないほど詳細なモデルを作成します。もう 1 つはボクセル コーン トレーシングです。これにより、Playstation で作成されたゲームでも、拡散反射による間接照明を使用できます。1 秒あたり 60 フレーム以上ではなく、そのようなデータ指向の手法を使用せずに 1 つのフレームをレンダリングするのに数分または数時間かかると人々が考えるものです。そのため、効果的な国防総省のアプローチにより、以前は不可能と考えられていた新しい機能が実際にソフトウェアにもたらされることがあります。もう 1 つはボクセル コーン トレーシングです。これにより、Playstation で作成されたゲームでも、拡散反射による間接照明を使用できます。1 秒あたり 60 フレーム以上ではなく、そのようなデータ指向の手法を使用せずに 1 つのフレームをレンダリングするのに数分または数時間かかると人々が考えるものです。そのため、効果的な国防総省のアプローチにより、以前は不可能と考えられていた新しい機能が実際にソフトウェアにもたらされることがあります。もう 1 つはボクセル コーン トレーシングです。これにより、Playstation で作成されたゲームでも、拡散反射による間接照明を使用できます。1 秒あたり 60 フレーム以上ではなく、そのようなデータ指向の手法を使用せずに 1 つのフレームをレンダリングするのに数分または数時間かかると人々が考えるものです。そのため、効果的な国防総省のアプローチにより、以前は不可能と考えられていた新しい機能が実際にソフトウェアにもたらされることがあります。

DOD の考え方は、より効率的であると見なされる場合、AoS 表現を利用する設計につながる可能性があります。AoS は、たとえば、ランダム アクセスが必要な場合や、インターリーブされたフィールドのすべてまたはほとんどがホットで、頻繁にアクセスおよび/または一緒に変更される場合に優れていることがよくあります。

また、これは私の考えに過ぎませんが、私の意見では、国防総省は最も効率的なデータ表現に最初から到達する必要はありません。必要なのは、必要に応じて最適化するための十分な余地を残すために、最も効率的なインターフェース設計に最初から到達することです。国防総省の考え方が提供する先見の明が欠けていると思われるソフトウェアの例は、次のようなデータを表すビデオ合成ソフトウェアです。

class IPixel
{
public:
    virtual ~IPixel() {}
    ...
};

上記のコードを一目見ただけで、効率的なデータ表現とアクセスを実現するための設計方法について、先見の明が著しく欠けていることがわかります。まず、32 ビットの RGBA ピクセルを考えると、仮想ポインターのコストは、ポインターのサイズと配置が 64 ビットであると仮定すると、1 つのピクセルのサイズの4 倍になります (64 ビットの vptr + 32 ビットのピクセル データ + 32 - vptr のアラインメントのためのパディングのビット)。したがって、国防総省の考え方を適用する人は、一般的に、疫病のようなインターフェイス設計を避けます. ただし、多くの異なるピクセル形式の画像に同じコードを利用できるなど、抽象化の恩恵を受ける可能性があります。しかし、その場合、私はこれを期待します:

class IImage
{
public:
   virtual ~IImage() {}
   ...
};

...これは、vptr、仮想ディスパッチ、連続性が失われる可能性などのオーバーヘッドを、ピクセルごとに支払われるものではなく、画像全体 (おそらく数百万ピクセル) のレベルまで些細なものにします。

通常、国防総省の考え方は、粒状ではなく、より粗いインターフェース設計につながる傾向があります (ピクセルのコンテナー、場合によってはコンテナーのコンテナーを表す画像インターフェースの場合のように、コンテナー全体のインターフェース)。主な理由は、次のようなコードベースがある場合、一元的に最適化する余地があまりないためです。

ここに画像の説明を入力

どこでも一度に多くのボールの処理をマルチスレッド化したいとしましょう。ボールを個別に使用してコードベース全体を書き直さなければ、それはできません。別の例として、ボールの表現を AoS から SoA に変更したいとします。それには、以前の設計を使用してコードベース全体に合わせてBall書き直す必要があります。GPU でボールを処理する場合も同様です。したがって、通常、国防総省の考え方は、次のような粗い設計を好む傾向があります。BallsBallBalls

ここに画像の説明を入力

この 2 番目のケースでは、ボールを並列処理したり、SoA で表現したりするために必要なすべての最適化を適用できます。コードベースを書き直す必要はありません。しかし、そうは言っても、実装はAoS を使用してBalls各個人をプライベートに保存する可能性があります。Ball

class Balls
{
public:
    ...
private:
    struct Ball
    {
        ...
    };
    vector<Ball> balls;
};

... か否か。Ballsこの時点では、コードベースの残りの部分に影響を与えることなく、好きなすべてのプライベート実装を自由に変更できるため、それほど重要ではありません。

最後に、ベンチマークについて、それは何をしますか? 基本的に、一連の単精度浮動小数点数をループし5てそれらに追加しています。その場合、float の配列を 1 つ保存するか、1000 を保存するかに関係なく、実質的な違いはありません。より多くの配列を格納すると、必然的にオーバーヘッドが追加され、すべての浮動小数点数をループして 5 を追加するだけであれば、何のメリットもありません。

SoA 表現を利用するために、すべてのフィールドに対してまったく同じことを行うコードを書くことはできません。SoA は通常、効率的な SIMD 命令 (手書きまたはオプティマイザー) 4 つ以上のボールを一度に変換します5。フロートのボートロードに単純に追加するのではありません。これらは、すべてのフィールドがホットではない場合に特に優れています。たとえば、物理システムがパーティクルのスプライト フィールドに関心がない場合などです (使用しないためだけにキャッシュ ラインにロードするのは無駄です)。したがって、SoA 担当者と AoS 担当者の違いをテストするには、実際の違いを確認するために十分に現実的な種類のベンチマークが必要です。

于 2018-01-06T14:28:14.813 に答える