2

グラフィカル ユーザー インターフェイスを持つ C++ の大規模なプロジェクトに取り組んでいます。ユーザー インターフェイスは、オブザーバー パターンに依存する設計パターン (MVVM/MVC) を使用します。

私の問題は、現在、モデルのどの部分を観察可能にするかを予測する方法がないことです。そして、たくさんのパーツがあります。

この問題により、いくつかの方向に引っ張られていることに気づきました。

  1. 通知をサポートしないバックエンド クラスを開発すると、Open-Closed 原則に違反していることに気付くでしょう。
  2. すべてのモデル クラスとそのすべてのデータ メンバーへの通知のサポートを提供した場合、実際に必要なのはこのサポートの一部のみであるため (この部分は不明ですが)、正当化されないほどの膨大なパフォーマンス コストが発生します。
  3. すべての非 const メソッドを仮想化し、これらのメソッドにベースポインターを介してアクセスすることによって、拡張のサポートのみを提供する場合も同様です。これには、可読性にもコストがかかります。

この 3 つのうち、(1.) の方がおそらく害が少ないと思います。

ただし、理想的なソリューションが実際にいくつかの言語 (間違いなく C++ ではない) に存在する必要があるように感じますが、それがどこでもサポートされているかどうかはわかりません。

私が考えていたユニコーンの解決策は次のようなものです: クラス Data が与えられた場合、 Data を観察可能にしようとするクライアントが次のようなことを行うことは可能ではないでしょうか?

@MakeObservable(データ)

コンパイル時の構造として。これにより、Data オブジェクトで addObserver を呼び出し、notifier を使用してデータ メンバーへのすべての割り当てを変更できるようになります。また、得たものに対してのみパフォーマンスで支払うようになります。

だから私の質問は2つあります:

  1. 私が述べた 3 つの選択肢のうち、(1.) はより少ないが必要な悪であると仮定するのは正しいでしょうか?
  2. 私のユニコーン ソリューションはどこかに存在しますか? 取り組んでいますか?それとも何らかの理由で実装できないでしょうか?
4

2 に答える 2

1

私の理解が正しければproperty、すべてのオブジェクトの潜在的にすべてのオブザーバブルにシグナル/通知を提供するコストに関心があります。

幸いなことに、すべてのオブジェクトのすべてのプロパティを使用して一般的なスレッド セーフな通知機能を格納すると、どの言語やシステムでも非常にコストがかかるため、幸運です。

大規模なプロジェクト (例: プラグインやスクリプト) に対して非常に潜在的に有用なオプションを締め出すことをお勧めします。これは実行時に安価です。オブジェクトの個々のプロパティよりも粗いレベルでシグナルを保存したい。

プロパティ変更イベント中に変更されたプロパティに関する適切なデータを渡すオブジェクトを 1 つだけ格納して、通知するクライアントをフィルター処理する場合は、はるかに安くなります。接続されたスロットに対していくつかの追加の分岐とより大きな集約を交換していますが、潜在的に高速な読み取りアクセスと引き換えに、非常に小さなオブジェクトを取得します。これは実際には非常に価値のある交換であることをお勧めします.

クライアントがオブジェクト全体ではなくプロパティに接続しているように感じられる方法でシステムを操作できるように、パブリック インターフェイスやイベント通知メカニズムを設計することもできます。 /proxy) を使用して、プロパティからオブジェクトへのバック ポインターが必要な場合、または余裕がある場合にスロットを接続します。

確信が持てない場合は、イベント スロットをプロパティにアタッチし、それらをプロパティ インターフェイスではなくオブジェクト インターフェイスの一部として変更することをお勧めします。わずかに異なるクライアントの美学 (私が実際に考えていないものは、単に「異なる」ほど便利ではないか、少なくともプロパティごとにバックポインターを排除するコストに見合う価値があるとは思いません)。

それは便利さとラッパータイプのものの領域にあります。ただし、C++ で MVP 設計を実現するために、開閉原理に違反する必要はありません。データ表現によって隅に詰め込まれないでください。パブリック インターフェイス レベルで多くの柔軟性があります。

メモリの圧縮 -- 使用した分だけ支払う

ここで効率が重要な役割を果たしていることを発見したら、それを支援するための基本的な考え方をいくつか提案します。

まず、オブジェクトにアクセサのようなものsomething()があるからといって、関連付けられたデータをそのオブジェクトに格納する必要があるわけではありません。そのメソッドが呼び出されるまで、どこにも保存する必要はありません。記憶が気になる場合は、外部のあるレベルに保存できます。

ほとんどのソフトウェアは、リソースを所有する集合体の階層に分解されます。たとえば、3D ソフトウェアでは、アプリケーション ルートが所有するシーン グラフが所有するメッシュが頂点を所有します。

使用されていないものに対してほとんどメモリ コストを支払わない設計が必要な場合は、データをより粗いレベルでオブジェクトに関連付ける必要があります。オブジェクトに直接保存すると、必要かどうかに関係なく、すべてのオブジェクトがsomething()戻り値に対して支払います。ポインターを使用して間接的にオブジェクトに格納する場合は、そのポインターに対して料金が発生しますsomething()が、使用されない限り、その全費用は発生しません。それをオブジェクトの所有者に関連付けると、検索コストがかかりますが、オブジェクトの所有者の所有者に関連付ける場合ほど高価ではありません。

したがって、十分に粗いレベルで関連付ければ、使用しないものに対して非常に無料に近いものを取得する方法が常にあります。細かいレベルではルックアップと間接的なオーバーヘッドを軽減し、粗いレベルでは使用しないもののコストを軽減します。

大規模イベント

数百万から数十億の要素が処理されるという大規模なスケーラビリティの懸念と、潜在的にそれらの一部がイベントを生成する必要があることを考えると、非同期設計を使用できる場合は、ここでそれを強くお勧めします. シングル ビット フラグが設定されたオブジェクトがイベントを生成するスレッドごとのイベント キューをロックフリーにすることができます。ビット フラグが設定されていない場合は、設定されません。

この種の延期された非同期設計は、定期的な間隔を提供するため、このようなスケールで役立ちます (または、書き込みロックと読み取りロックが必要ですが、書き込みは安価にする必要がありますが、他のスレッドだけでも可能です)その場合) キューの一括処理に全リソースをポーリングして専念する一方で、よりタイムクリティカルな処理はイベント/通知システムと同期せずに続行できます。

基本的な例

// Interned strings are very useful here for fast lookups
// and reduced redundancy in memory.
// They're basically just indices or pointers to an 
// associative string container (ex: hash or trie).

// Some contextual class for the thread storing things like a handle
// to its event queue, thread-local lock-free memory allocator, 
// possible error codes triggered by functions called in the thread,
// etc. This is optional and can be replaced by thread-local storage 
// or even just globals with an appropriate lock. However, while
// inconvenient, passing this down a thread's callstack is usually 
// the most efficient and reliable, lock-free way.
// There may be times when passing around this contextual parameter
// is too impractical. There TLS helps in those exceptional cases.
class Context;

// Variant is some generic store/get/set anything type. 
// A basic implementation is a void pointer combined with 
// a type code to at least allow runtime checking prior to 
// casting along with deep copying capabilities (functionality
// mapped to the type code). A more sophisticated one is
// abstract and overriden by subtypes like VariantInt
// or VariantT<int>
typedef void EventFunc(Context& ctx, int argc, Variant** argv);

// Your universal object interface. This is purely abstract:
// I recommend a two-tier design here: 
// -- ObjectInterface->Object->YourSubType
// It'll give you room to use a different rep for 
// certain subtypes without affecting ABI.
class ObjectInterface
{
public:
     virtual ~Object() {}

     // Leave it up to the subtype to choose the most
     // efficient rep.
     virtual bool has_events(Context& ctx) const = 0;

     // Connect a slot to the object's signal (or its property 
     // if the event_id matches the property ID, e.g.).
     // Returns a connection handle for th eslot. Note: RAII 
     // is useful here as failing to disconnect can have 
     // grave consequences if the slot is invalidated prior to 
     // the signal.
     virtual int connect(Context& ctx, InternedString event_id, EventFunc func, const Variant& slot_data) = 0;

     // Disconnect the slot from the signal.
     virtual int disconnect(Context& ctx, int slot) = 0;

     // Fetches a property with the specified ID O(n) integral cmps.
     // Recommended: make properties stateless proxies pointing
     // back to the object (more room for backend optimization).
     // Properties can have set<T>/get<T> methods (can build this
     // on top of your Variant if desired, but a bit more overhead
     // if so).
     // If even interned string compares are not fast enough for
     // desired needs, then an alternative, less convenient interface
     // to memoize property indices from an ID might be appropriate in
     // addition to these.
     virtual Property operator[](InternedString prop_id) = 0;

     // Returns the nth property through an index.
     virtual Property operator[](int n) = 0;

     // Returns the number of properties for introspection/reflection.
     virtual int num_properties() const = 0;

     // Set the value of the specified property. This can generate
     // an event with the matching property name to indicate that it
     // changed.
     virtual void set_value(Context& ctx, InternedString prop_id, const Variant& new_value) = 0;

     // Returns the value of the specified property.
     virtual const Variant& value(Context& ctx, InternedString prop_id) = 0;

     // Poor man's RTTI. This can be ignored in favor of dynamic_cast
     // for a COM-like design to retrieve additional interfaces the
     // object supports if RTTI can be allowed for all builds/modes.
     // I use this anyway for higher ABI compatibility with third
     // parties.
     virtual Interface* fetch_interface(Context& ctx, InternedString interface_id) = 0;
};

データ表現の核心的な詳細には立ち入らないことにします。要点は、データ表現が柔軟であるということです。重要なのは、必要に応じて変更する余地を自分で購入することです。オブジェクトを抽象化し、プロパティをステートレス プロキシとして保持する (オブジェクトへのバックポインターを除く) などにより、プロファイルを作成して最適化するための余裕が生まれます。

Context非同期イベント処理の場合、各スレッドには、このハンドルを介してコール スタックに渡すことができるキューが関連付けられている必要があります。プロパティの変更などのイベントが発生すると、オブジェクトはこのキューを介してイベントをプッシュできますhas_events() == true。同様に、connect必ずしもオブジェクトに状態を追加するとは限りません。Contextobject/event_id をクライアントにマップするを介して、連想構造を作成できます。disconnectまた、その中央のスレッド ソースからも削除します。スロットをシグナルに接続/シグナルから切断する行為でさえ、イベント キューにプッシュして、中央のグローバルな場所で処理し、適切な関連付けを行うことができます (オブザーバーを持たないオブジェクトがメモリ コストを支払うことを防ぎます)。

このタイプの設計を使用する場合、各スレッドは、スレッド イベント キューにプッシュされたイベントをスレッド ローカル キューからグローバル キューに転送するスレッドの終了ハンドラーをエントリ ポイントに持つ必要があります。これにはロックが必要ですが、激しい競合を回避し、パフォーマンスが重要な領域でイベント処理によって各スレッドの速度が低下しないようにするために、あまり頻繁に実行することはできません。thread_yield同様に、スレッド ローカル キューから長寿命のスレッド/タスクのグローバル キューに転送するような設計で、ある種の機能を提供する必要があります。

グローバル キューは別のスレッドで処理され、接続されたスロットに適切なシグナルをトリガーします。そこでは、キューが空でない場合はキューの一括処理に集中でき、空である場合はスリープ/解放します。これらすべての要点は、パフォーマンスを向上させることです。キューへのプッシュは、オブジェクト プロパティが変更されるたびに同期イベントを送信する可能性に比べて非常に安価であり、大規模な入力を扱う場合、これは非常にコストのかかるオーバーヘッドになる可能性があります。 . そのため、単純にキューにプッシュするだけで、そのスレッドはイベント処理に時間を費やさずに、別のスレッドに延期できます。

于 2015-05-07T07:09:11.980 に答える