7

アプリケーションアーキテクチャの非常に低いレベルのポイントでコードをマイクロ最適化しようとしています。これが私の具体的なシナリオです。

  • グラフファイル(ノード、エッジ、隣接エントリなど)を解析するパーサークラスがあります。
  • ファイル形式はバージョン管理されているため、バージョンごとに個別のクラス(ParserV1、ParserV2、...)として実装されるパーサーが存在します。
  • パーサーは、アプリケーションの一部の上位層に同じ機能を提供します。したがって、それらは同じ「インターフェース」を実装します。
  • C ++では、すべての関数が純粋な仮想である抽象クラスなどのインターフェイスを実装します。
  • 仮想関数は別のメモリルックアップを必要とし、コンパイル時に静的にバインドすることはできず、さらに重要なことですが、パーサークラスで小さなメソッドをインライン化することはできないため、従来のサブクラス化イディオムを使用しても、私が達成できる最高のパフォーマンス。

[考えられる解決策を説明する前に、ここでマイクロ最適化を行っている理由を説明したいと思います(この段落はスキップできます):パーサークラスには多くの小さなメソッドがあります。「小さい」とは、あまり機能しないことを意味します。 。それらのほとんどは、キャッシュされたビットストリームから1バイトまたは2バイト、あるいは1ビットのみを読み取ります。したがって、非常に効率的な方法でそれらを実装できるはずです。関数呼び出しは、インライン化されたときに、ほんの一握りのマシンコマンドしか必要としません。メソッドは非常に大きなグラフ(世界規模の道路網)でノード属性を検索するため、アプリケーションで頻繁に呼び出されます。これは、ユーザーのリクエストごとに約100万回発生する可能性があり、そのようなリクエストは次のように高速である必要があります。可能。]

ここに行く方法はどれですか?私は問題を解決するために次の方法を見ることができます:

  1. 純粋仮想メソッドを使用してインターフェースを作成し、それをサブクラス化します。パフォーマンスが低下します。
  2. そのようなインターフェースを書かないでください。各パーサーは、それ自体で同じメソッドを定義します。(パーサーを使用する)上位層には、各バージョンのサブクラスへの(メンバーとしての)ポインターがあります。最初に、使用する必要がある特定のパーサーをインスタンス化します。関数にアクセスするときはいつでも、スイッチブロックを使用して、パーサーインスタンスを明示的なサブクラスにキャストします。パフォーマンスは向上しますか?(if / switchブロックと仮想テーブルルックアップ)。
  3. 2つのソリューションを組み合わせる1.+2 .:パフォーマンスがそれほど重要ではない、めったに使用されないメソッド用の純粋仮想メソッドを使用してインターフェースを作成します。重要な場合は、仮想メソッドを提供せずに、2番目のメソッドを使用してください。
  4. 2.の改善:抽象クラスに非仮想メソッドを提供します。抽象クラス(独自の実行時型情報の一種)のメンバー変数としてバージョン番号を保持し、これらのメソッドでif/switchブロックとキャストを実装します。次に、サブクラスのメソッドを呼び出します。これにより、インライン化と静的バインディングの両方が提供されます。

この問題を解決するためのより良い方法はありますか?このためのイディオムはありますか?

明確にするために、私はバージョンに依存しない(少なくとも今までは)多くの関数を持っているので、いくつかのスーパークラスに完全に適合しています。ほとんどの関数に標準のサブクラス化設計を使用しますが、この質問では、バージョンに依存する関数を最適化するためのソリューションのみを取り上げます。(それらのいくつかはあまり頻繁に呼び出されず、もちろんこれらの場合に仮想メソッドを使用できます。)これに加えて、パーサークラスにパフォーマンスが必要なメソッドとパフォーマンスが必要でないメソッドを決定させるというアイデアは好きではありません。 。(そうすることは可能ですが。)

4

2 に答える 2

3

うまく機能する可能性のあるオプションの1つは、次のとおりです。各パーサークラスに同じシグネチャを持つメソッドを定義させますが、他のクラスとは完全に独立しています。次に、これらの同じ関数をすべて仮想的に実装する2次クラス階層を導入し、各メソッド呼び出しを具象パーサーオブジェクトに転送します。このように、パーサーの実装にはインライン化のすべての利点があります。クラスの観点からは、すべての呼び出しを静的に解決できるのに対し、クライアントはポリモーフィズムの利点を得ることができます。これは、すべてのメソッド呼び出しが動的に適切な型に解決されるためです。

これを行う際の落とし穴は、余分なメモリを使用することです(ラッパーオブジェクトはスペースを占有します)。また、パーサー関数を呼び出すときに、呼び出しが行われるため、少なくとも1つの余分な間接参照が含まれる可能性があります。

クライアント→ラッパー→実装

クライアントからメソッドを呼び出す頻度によっては、この実装が非常にうまく機能する場合があります。

テンプレートを使用すると、ラッパーレイヤーを非常に簡潔に実装できます。アイデアは次のとおりです。メソッドfA、fB、およびfCがあるとします。次のように基本クラスを定義することから始めます。

class WrapperBase {
public:
    virtual ~WrapperBase() = 0;

    virtual void fA() = 0;
    virtual void fB() = 0;
    virtual void fC() = 0;
};

ここで、次のテンプレートタイプをサブクラスとして定義します。

template <typename Implementation>
    class WrapperDerived: public WrapperBase {
private:
    Implementation impl;

public:
    virtual void fA() {
        impl.fA();
    }
    virtual void fB() {
        impl.fB();
    }
    virtual void fC() {
        impl.fC();
    }
};

今、あなたはこのようなことをすることができます:

WrapperBase* wrapper = new WrapperDerived<MyFirstImplementation>();
wrapper->fA();
delete wrapper;

wrapper = new WrapperDerived<MySecondImplementation>();
wrapper->fB();
delete wrapper;

WrapperDerivedつまり、テンプレートをインスタンス化するだけで、コンパイラーがすべてのラッパーコードを生成できます。

お役に立てれば!

于 2012-07-02T22:42:35.377 に答える
2

まず、当然のことながら、コードのプロファイルを作成して、特定のケースでvcallのパフォーマンスが低下する量を把握する必要があります(最適化が弱くなる可能性があることを除けば)。

最適化のテーマは別として、仮想関数呼び出し(またはほぼ同じポインター変数で関数を呼び出す)をコンパイル時を呼び出すスイッチに置き換えることで、パフォーマンスが大幅に向上することはほとんどないと確信しています。さまざまな場合の既知の機能。

本当に大幅な改善が必要な場合は、これらが最も有望なバリアントです。

  1. より複雑な機能を有効にするために、インターフェースを再設計してみてください。たとえば、単一の頂点を読み取る関数がある場合は、一度にN個の頂点(最大)を読み取るように変更します。等々。

  2. (パーサーを使用する)解析コード全体をtemplateクラス/関数にすることができます。これは、テンプレートパラメーターを使用して必要なパーサーをインスタンス化します。ここでは、インターフェースも仮想関数も必要ありません。最初に(バージョンを識別する場所)-switch認識されたバージョンごとに、適切なテンプレートパラメーターを使用してこの関数を呼び出します。

後者はおそらくパフォーマンスの観点から優れているでしょう、OTOHこれはコードサイズを増やします

編集:

(2)の例を次に示します。

template <class Parser>
void MyApplication::HandleSomeRequest(Parser& p)
{
    int n = p.GetVertexCount();
    for (iVertex = 0; iVertex < n; iVertex++)
    {
        // ...    
        p.GetVertexEdges(iVertex, /* ... */);    
        // ...    
    }
}

void MyApplication::HandleSomeRequest(/* .. */)
{
    int iVersion = /* ... */;
    switch (iVersion)
    {
    case 1:
        {
            ParserV1 p(/* ... */);
            HandleSomeRequest(p);
        }
        break;

    case 2:
        {
            ParserV2 p(/* ... */);
            HandleSomeRequest(p);
        }
        break;

    // ...
    }
}

クラスParserV1などには機能ParserV2がありませvirtual。また、インターフェイスを継承しません。それらは、などのいくつかの関数を実装するだけGetVertexCountです。

于 2012-07-02T22:48:39.710 に答える