web-dev-qa-db-ja.com

C ++依存性注入とメモリ使用量

これらの2つのクラスを含む架空のC++コードがあります。

  • マスター:大きく、多くのことを実行し、インスタンスを1つだけ持つことを意図しています。
  • スレーブ:まったく逆です。また、多くのことを実行できますが、それは小さく、多くのインスタンスがあります。

すべてのスレーブはマスターにアクセスする必要があるため、コンストラクターを通じて注入されます。

class Slave {
    private:
        // Few small attributes
        Master& master;
    public:
        Slave(Master& master) master(master) { }
        // May have lots of methods...
}

多くのスレーブがあり、それぞれがマスターへの参照を保持しているため、同じものを指すポインターで大量のメモリが浪費されます。 C++コンパイラーがSlaveクラスからそのMaster& master属性を最適化する方法を見つけることができると思いたいのですが、そうは思わない-どうか、私を修正してくださいmは間違っています。

解決策はMasterクラスをシングルトンに変換することですが、これはアンチパターンと広く見なされています。 Master& master属性を静的ポインタに変換するのが、おそらくより良いアプローチだと思います。

class Slave {
    private:
        // Attributes...
    public:
        static Master* master;
        Slave() { }
        // Methods...
}

参照をポインタに変換する必要があるのは残念ですが、少なくともこれにより、メモリの浪費がなくなり、マスターを「注入」してモックをテストに使用できるようになります。

皆さんはどう思いますか?

6
Milack27

まあ、あなたは何かが間違っているのは正しいです。しかし、メモリ使用量を心配することで解決するかどうかは、私には非常に疑問です。

これらの参照の規模でメモリの問題があることを示すいくつかの実際のデータを指摘できない限り、私はそれについて心配しません。これは時期尚早の最適化です。

だからといって、ここに問題がないわけではありません。マスターはスレーブが必要のない部分に依存するように強制しています。これは、インターフェース分離の原則に違反しています。必要ない場合は、遠ざけてください。

スレーブは、依存関係を明示的にするに値します。それは彼らがプリミティブしか取れないという意味ではありません(プリミティブへの執着を参照)独自のパラメーターオブジェクトを取得できます。ただし、これらのパラメーターオブジェクトは、スレーブの個々のニーズに合わせて調整する必要があります。 1つのサイズですべてに対応できるわけではありません。

奴隷を解放した後の記憶を心配します。

12
candied_orange

ポインタ参照(Master* ref)問題になるのは、組み込みプログラミングなどの制約された環境にいる場合です。その場合、すべてのMastersにSlaveインスタンスを提供するために、静的アクセサーを備えた標準のシングルトンを使用する方が良いでしょう。そうすれば、心配するポインタは1つだけになります。

さらに、パターンとしての依存性注入は、おそらく組み込み環境に適したツールではないだろうと私は主張します。

依存性注入が間違った解決策となるもう1つの方法は、Masterが状態を格納せず、単にいくつかの純粋な関数のコンテナーである場合です。クラス内に配置することなく標準関数を使用できるため、おそらくC++では一般的ではありませんが、C#およびJavaでは言語設計により十分に一般的です。

Dependency Injectionはサーバー側のコードで一般的になり、デスクトップアプリケーションで動作するように拡張されました。通常、クラスに参照(またはポインタ)を格納するコストは、アプリケーション全体が使用しているメモリの量と比較すると、わずかです。

議論のために、Masterがアプリケーションの状態を格納するとします。その場合は、次のことが当てはまる可能性が高いです。

  • Masterによって制御されるメモリの量は、ほとんどの場合、Slaveオブジェクト内のすべての参照からの累積バイト数よりも大きくなります。
  • Slaveによって制御されるメモリの量は、おそらくMasterへのポインタよりも大きくなります。
  • アプリケーションが使用するメモリの総量は、おそらく許容範囲内です。
  • MasterSlaveの関係を簡単にテストできるソリューションがあります。これは、DIを使用するメリットの1つです。
  • MasterSlaveオブジェクトに明示的に渡すことにより、シングルトンへのアクセスを少しよく制御し、分離を向上させることができます。

ある時点で、パターンの使用をできるだけ客観的に見て、パターンを使用するために行うトレードオフに価値があるかどうかを判断する必要があります。依存性注入がオーバーヘッドに値しない場合があります。オーバーヘッドがそれだけの価値がある場合があります。それがあなたのアプリケーションに適切なツールである場合、あなただけが答えることができます。もしそうなら、良いことも悪いことも受け入れる。これはあなたがケーキを持ってそれを食べることもできるケースではありません。

4
Berin Loritsch

私はパフォーマンスが非常に重要な分野で働いているので、パフォーマンスと引き換えに保守性を犠牲にして、誰にとってもやりがいのあるやりかたではないかもしれないので、私の考えは少し歪んでいるかもしれません。私の場合、効率的なインターフェース設計を無視すると(効率的な実装ではなく、設計を最適化するために設計を変更する必要がない限り、実装の効率が悪くなる可能性があります)、インターフェースの再設計が強く必要になるため、多くのメンテナンスコストが発生します。また、設計がフードの背後に最適化のための十分な呼吸の余地を残さなかった場合のカスケードの破損への対処。オブジェクトが非常に細かいレベルでモデル化されている場合は、それは起こりません。レイトレーシングなどの分野で作業する場合、速度は非常に質の高い指標です。遅いですが正しいレイトレーサーは、ユーザーにとっては高速ですが完全に壊れているレイトレーサーと同じくらい価値がありません。

とにかく、オブジェクトへの依存性注入が高すぎる場合、私がそれを見る方法は、オブジェクト自体が非常に細かくモデル化されているということです。

より粗いレベルでデザインをモデリング

パフォーマンスが重要な領域では、Imageのレベルではなく、Pixelのレベルで、ParticleSystemのレベルではなく、Particleのレベルで、Monstersのレベルで、より粗いレベルでオブジェクトとインターフェイスをモデル化する必要がよくあります。 Monster。したがって、Masterまたは注入しようとしているものへのポインタを保存するのが個々のSlaveにとって高すぎる場合(これは、数百万以上のスレーブがあるか、スレーブをできるだけ小さくして、あなたは単一のキャッシュラインで行うことができます)、そして私が問題を見る方法は、障害がDIにあるということではなく、Slaveが非常に細かいレベルでモデル化されていることです。

代わりに、スレーブの基本的なコレクションをモデル化するSlavesに変換する価値があるかもしれません。

  • たとえばvector<Slave>だけ--Slaveが圧縮されている場合は特に、vector(ゼロバイトに非常に近い)ごとに追加のメモリコストが実質的に発生しない最も安価で簡単なデータ構造。その後、スレーブに関連する特定の操作にさらに高度なデータ構造が必要な場合は、セットやマップなどの他のデータ構造を使用できます。

数十のスレーブまたは数百、数千、数百万の単一のポインタのオーバーヘッドしかない場合、DIのポインタのコストは完全に簡単になります。さらに、インターフェイスがSlavesまたはSlavesの範囲で動作し、一度に単一のSlaveではない場合(Slavesインターフェイスでの重要でない実行パスが特定のスレーブに単純なスカラー操作を適用するための便宜のために追加で提供される場合があります) 、あなたはあなたの奴隷に関連する機能を最適化するために非常に多くの呼吸の余地があります。

最適なベクトル化とキャッシュのためのスレーブを構成するデータフィールドのハイブリッドAoSoA表現など、各Slaveを個別のオブジェクトとして作成した場合には不可能だった方法で多くのスレーブを効率的に格納するためのデータ表現を考え出すこともできますアクセスされていないスレーブのデータフィールドをまたぐ必要がないようにホット/コールドフィールド分割を使用したベクトル化されたコードを使用して、複数のスレッドにわたって瞬く間に数百万のスレーブを通過できるランダムアクセスとシーケンシャル処理の両方のヒット実行のクリティカルパスで頻繁に。また、スカラーSlavesオブジェクトではなくSlaveオブジェクトの大まかなレベルでモデル化する場合、デザインを変更したり、外部依存関係を壊したりすることなく、これらの最適化をすべて心のコンテンツに適用できます。

class Slaves
{
public:
    // Public interface. Does everything you could formerly do
    // with slaves individually, but now in bulk and optionally
    // to multiple slaves at once.

private:
    // All the privates can be changed to our heart's content
    // without breaking the public interface. We would not be
    // able to explore such data representations to aggregate
    // Slave data if we had a 'Slave' object instead.
    struct SlaveData
    {
        // AoSoA
        float16 x4[4];
        float16 y4[4];
    };

    // Hot fields of all slaves, accessed frequently in critical
    // paths. Uses AoSoA for rapid sequential and reasonable
    // random access. Each entry holds 4 slaves worth of data.
    vector_aligned64<SlaveData> data; 

    // Names of all slaves -- cold field hoisted away from the
    // critical data and stored in parallel since it is not accessed by
    // performance-critical execution paths. This avoids slowing down
    // the critical paths with additional cache misses and page faults
    // in sequential loops through the hot fields above.
    vector<string> slave_names;

    // Pointer for DI.
    Master* master;
};

上記の例は極端なものであり、測定するまでは実装の詳細の効率にそれほど関係する必要のない最初のドラフトの提案ではありません。これは、Slave集約インターフェースの代わりに個別のSlavesオブジェクトとインターフェースがあった場合に実行できなかった方法で最適化するために、この設計でどのように余裕があるかを示しています。

そのような設計でポリモーフィズムが必要な場合は、実際には、ポリモーフィックスカラーの代わりにポリモーフィック集計を作成します。たとえば、DogMammalを継承する代わりに、DogsMammalsを継承し、DogsのコレクションをMammalsのコレクションからMammalsインターフェースのコレクションとして扱うことができます。おそらくすでにお分かりのように、これには、考えられる最もパフォーマンスが重要なシステムでも、動的ディスパッチが問題になることのないように、動的ディスパッチを簡単にする利点があります。この方法では、vtableキャッシュミスを最小限に抑えるために、少なくともサブタイプでポリモーフィックコンテナーを並べ替えるようなゲームプログラマーのトリックはまったく不要です。各ベースポインターが実際に集約を指し、各仮想メソッドが複数の要素に適用されるかさばるロジックをすでに呼び出しているためです。一度に(個々の要素レベルで動的ディスパッチなし)。スレッドロックも簡略化されます。これは、レベルが細かすぎてロックが頻繁に行われるリスクを冒すことなく適切にロックできるほど粗いかさばる集約操作があるためです。

メンテナンス費用

このタイプの設計には、インターフェイスを使用するクライアントの数に比例して、メンテナンスコストがかかります。一度に1つのスレーブにのみ適用されるスカラー操作は、現在の場合よりもさらに遅くなる可能性があるため、クライアントは、複数のスレーブに対して何をしているのかをできるだけ頻繁に表現する方法について考え始める必要があります。スカラーSlaveオブジェクトがありました。彼らは、スレーブのどのサブ範囲を並行して変換するかを示すかさばる集約操作に渡すために、事前にインデックスペアの範囲のリストを作成する必要があるかもしれません。クライアントがセカンダリスレーブを管理するために、セカンダリデータ構造を管理する必要があります。興味があります。単一のSlaveインターフェースで達成できるものと比較して、インターフェースの使用は常に多少扱いにくいものになります。これは、OpenGLで頂点バッファーオブジェクトを操作する煩わしさに似ています-頂点を一括で処理する方がはるかに効率的ですが、それらを使用すると、頂点のリストをループ処理するだけではコストがかかり、このような便利な方法で、クライアントコードのイミディエイトモードで個別に一度に1つずつレンダリングします。より便利な重要でないケースがたくさんある場合は、SlaveProxyクラスのように作成できます。このクラスは、スカラーインターフェイスを提供しますが、インデックスとSlaves集合体へのポインター以外は何も格納せず、非スカラー操作を呼び出します。 n番目のスレーブを処理するためのインデックス範囲は[n, n+1)のみです。

将来の最適化のための無限の呼吸室を備えた設計

したがって、経験に基づく本物のパフォーマンスの懸念がある場合(プロファイリングメトリック、またはスレーブが処理される頻度と通常いくつが保存されるかについての十分な知識のいずれか)、そのメリットをお伝えします疑いの余地はありますが、設計レベルですべての懸念に対処する最も有用な最適化戦略は、Slavesオブジェクトを設計し、スカラーSlaveオブジェクトを解体することです。この集約インターフェースは、一度に複数のスレーブに適用される操作でスレーブにすべてを行う責任を持ち、クリティカルでないパスの便宜のためにのみ、個々のスレーブに適用されるスカラー操作を行います。これにより、1つのスレーブ用に保存したり1つのスレーブで実行したりすることが、コードの既存のデザインとインターフェイスを変更せずに大幅に最適化することが不可能で、潜在的に大きなスレーブセクションを書き直さなければならないデザインに陥ることはありません。コードベース。

2
user204677