6

データのサイズとクラスの使用状況に応じて、データのコピーよりも移動セマンティクスの使用を検討し始める時期を知りたいと思っています。たとえば、Matrix4 クラスの場合、次の 2 つのオプションがあります。

struct Matrix4{
    float* data;

    Matrix4(){ data = new float[16]; }
    Matrix4(Matrix4&& other){
        *this = std::move(other);
    }
    Matrix4& operator=(Matrix4&& other)
    {
       ... removed for brevity ...
    }
    ~Matrix4(){ delete [] data; }

    ... other operators and class methods ...
};

struct Matrix4{
    float data[16]; // let the compiler do the magic

    Matrix4(){}
    Matrix4(const Matrix4& other){
        std::copy(other.data, other.data+16, data);
    }
    Matrix4& operator=(const Matrix4& other)
    {
       std::copy(other.data, other.data+16, data);
    }

    ... other operators and class methods ...
};

「手動で」メモリの割り当てと割り当て解除を行う必要があるオーバーヘッドがあると思います。このクラスを使用するときに移動構造に実際にヒットする可能性があるとすれば、メモリサイズが非常に小さいクラスの推奨される実装は何ですか? 本当に常にコピーよりも優先される移動ですか?

4

3 に答える 3

9

最初のケースでは、行列がスタック上に構築されている場合でも、ヒープからメモリを動的に割り当てるため、割り当てと解放は高価であり、移動は安価です (ポインターをコピーするだけです)。

2 番目のケースでは、割り当てと解放は安価ですが、移動は高価です。実際にはコピーであるためです。

したがって、アプリケーションを作成していて、そのアプリケーションのパフォーマンスだけを気にする場合、「どちらが優れているか? 」という質問に対する答えは、マトリックスを作成/破棄する量と、マトリックスをコピー/移動する量に依存する可能性があります。いずれにせよ、推測を裏付けるために独自の測定を行ってください。

測定を行うことで、移動が行われると予想される場所でコンパイラが多くのコピー/移動省略を行っているかどうかも確認できます。結果は予想に反する場合があります。

また、キャッシュの局所性はここに影響を与える可能性があります。ヒープ上のマトリックスのデータにストレージを割り当てる場合、要素ごとに処理したい 3 つのマトリックスをスタック上に作成すると、かなり分散したメモリ アクセス パターンが必要になる可能性があります。より多くのキャッシュ ミスが発生します。

一方、スタックにメモリが割り当てられている配列を使用している場合、同じキャッシュ ラインがそれらすべてのマトリックスのデータを保持できる可能性が高く、キャッシュ ヒット率が高くなります。ヒープ上の要素にアクセスするには、最初にdataポインターの値を読み取る必要があるという事実は言うまでもありません。これは、要素を保持しているメモリ領域とは異なるメモリ領域にアクセスすることを意味します。

繰り返しになりますが、この話の教訓は次のとおりです。独自の測定を行います

一方、ライブラリを作成していて、クライアントが実行する構築/破壊と移動/コピーの数を予測できない場合は、そのような2 つのマトリックス クラスを提供し、共通の動作を基本クラスに分解できます。 - おそらく基本クラスのテンプレート

これにより、クライアントに柔軟性が与えられ、十分に高度な再利用が可能になります。すべての共通メンバー関数の実装を 2 回記述する必要はありません。

このようにして、クライアントは、使用しているアプリケーションの作成/移動プロファイルに最適なマトリックス クラスを選択できます。


アップデート:

DeadMGがコメントで指摘しているように、動的割り当てアプローチに対する配列ベースのアプローチの利点の 1 つは、動的割り当てアプローチでは、生のポインター 、newおよびdeleteを使用して手動でリソース管理を行っていることです。これにより、ユーザー定義のデストラクタ、コピー コンストラクタ、移動コンストラクター、コピー代入演算子、および移動代入演算子。

を使用している場合、これをすべて回避できますstd::vector。これにより、メモリ管理タスクが実行され、これらすべての特別なメンバー関数を定義する負担から解放されます。

これは、手動のメモリ管理を行う代わりに使用することを提案するという単なる事実std::vector-設計とプログラミングの実践に関しては良いアドバイスである限り-は質問に答えませんが、元の答えはそうすると思います.

于 2013-04-21T09:07:33.493 に答える