web-dev-qa-db-ja.com

パフォーマンスを低下させることなく、Pimplバリエーションを実装できますか?

Pimplの問題の1つは、それを使用するとパフォーマンスが低下することです(追加のメモリ割り当て、不連続なデータメンバー、追加の間接参照など)。私は、pimplのすべての利点を得られないという犠牲を払ってこれらのパフォーマンスのペナルティを回避するpimplイディオムのバリエーションを提案したいと思います。アイデアは、クラス自体にすべてのプライベートデータメンバーを残し、プライベートメソッドのみをpimplクラスに移動することです。基本的なpimplと比較した場合の利点は、メモリが連続している(追加の間接参照がない)ことです。 pimplをまったく使用しない場合と比較した場合の利点は次のとおりです。

  1. プライベート関数を非表示にします。
  2. これらのすべての関数が内部リンケージを持ち、コンパイラーがより積極的に最適化できるように構造化できます。

だから私の考えは、pimplをクラス自体から継承させることです(私が知っている少し奇妙に聞こえますが、私に耐えてください)。次のようになります。

A.hファイル:

class A
{
    A();
    void DoSomething();
protected:  //All private stuff have to be protected now
    int mData1;
    int mData2;
//Not even a mention of a PImpl in the header file :)
};

A.cppファイル:

#define PCALL (static_cast<PImpl*>(this))

namespace //anonymous - guarantees internal linkage
{
struct PImpl : public A
{
    static_assert(sizeof(PImpl) == sizeof(A), 
                  "Adding data members to PImpl - not allowed!");
    void DoSomething1();
    void DoSomething2();
    //No data members, just functions!
};

void PImpl::DoSomething1()
{
    mData1 = bar(mData2); //No Problem: PImpl sees A's members as it's own
    DoSomething2();
}

void PImpl::DoSomething2()
{
    mData2 = baz();
}

}
A::A(){}

void A::DoSomething()
{
    mData2 = foo();
    PCALL->DoSomething1(); //No additional indirection, everything can be completely inlined
}

私が見る限り、これを使用することによるパフォーマンスのペナルティはまったくありませんが、pimplはありません。いくつかの可能なパフォーマンスの向上とよりクリーンなヘッダーファイルインターフェイス。これが標準のpimplと比較した場合の1つの欠点は、データメンバーを非表示にできないため、それらのデータメンバーを変更しても、ヘッダーファイルに依存するすべての再コンパイルがトリガーされることです。しかし、私がそれを見る方法では、メモリ内でメンバーを隣接させることの利点またはパフォーマンス上の利点を得る(または this hack -"Why Attempt#3 is Deplorable"を実行する)もう1つの注意点は、Aがテンプレートクラスの場合、構文が煩わしいことです(mData1を直接使用することはできません。これを行う必要があります-> mData1、依存する型のtypenameとおそらくテンプレートキーワードの使用を開始する必要があります)およびテンプレート化されたタイプなど)。さらにもう1つの注意点は、元のクラスではプライベートを使用できなくなり、保護されたメンバーしか使用できないため、単なる継承だけでなく、継承クラスからのアクセスを制限できないことです。私は試しましたが、この問題を回避できませんでした。たとえば、フレンド宣言を匿名の名前空間で実際のpimplクラスを定義できるように広くすることを期待して、pimplをフレンドテンプレートクラスにしようとしましたが、うまくいきません。データメンバーをプライベートに保ち、匿名の名前空間で定義された継承pimplクラスがそれらにアクセスできるようにする方法について誰かが考えている場合は、ぜひご覧になってください!それは私の主な予約をこれを使用することから排除します。

しかし、私はこれらの警告が私が提案するものの利益のために許容できると感じています。

この「関数のみのpimpl」イディオムへの参照をオンラインで探しましたが、何も見つかりませんでした。私は人々がこれについてどう思うか本当に興味があります。これに関する他の問題や、これを使用すべきでない理由はありますか?

更新:

私は この提案 を見つけました。多かれ少なかれ、正確に私が何であるかを達成しようとしていますが、標準を変更することによってそうしています。私はその提案に完全に同意し、それが標準になることを願っています(私はそのプロセスについて何も知らないので、それがどのくらいの確率で発生するかわかりません)。組み込みの言語メカニズムを使用してこれを実行できるようにしたいのですが。この提案は、私よりもはるかに上手に達成しようとしていることの利点についても説明しています。また、私の提案のようにカプセル化を破る問題もありません(プライベート->保護されています)。それでも、その提案が標準になるまで(それが発生する場合)、私が提案したことにより、リストされた警告に従って、これらの利点を得ることが可能になると思います。

UPDATE2:

答えの1つは、LTOをいくつかの利点(私が推測しているより積極的な最適化)を得るための可能な代替手段として言及しています。さまざまなコンパイラ最適化パスで何が起こっているのか正確にはわかりませんが、結果のコードには少し経験があります(私はgccを使用しています)。元のクラスにプライベートメソッドを置くだけで、外部リンクが必要になります。

私はここで間違っているかもしれませんが、私が解釈する方法は、すべての呼び出しインスタンスがそのTU内に完全にインライン化されている場合でも、コンパイル時オプティマイザは関数を削除できないということです。何らかの理由で、リンクされたバイナリ全体のすべての呼び出しインスタンスがすべてインライン化されているように見えても、LTOでも関数定義の削除を拒否します。何らかの理由で関数ポインターを使用して関数を呼び出すかどうかがリンカーにわからないためであるとの言及がいくつか見つかりました(リンカーがそのメソッドのアドレスが取得されないことが理解できない理由はわかりませんが) )。

私の提案を使用して、それらのプライベートメソッドを匿名の名前空間内のpimplに配置する場合、これは当てはまりません。それらがインライン化された場合、関数は(-finline-functionsを含む-O3を使用して)オブジェクトファイルに表示されません。

オプティマイザーは、関数をインライン化するかどうかを決定するときに、コードサイズへの影響を考慮して、それを理解しています。したがって、私の提案を使用して、オプティマイザがこれらのプライベートメソッドをインライン化できるように、少し「安く」しています。

9
dcmm88

Pimplパターンのセールスポイントは次のとおりです。

  • 完全なカプセル化:インターフェイスオブジェクトのヘッダーファイルに記載されている(プライベート)データメンバーはありません。
  • 安定性:パブリックインターフェイス(C++ではプライベートメンバーを含む)を解除するまで、インターフェイスオブジェクトに依存するコードを再コンパイルする必要はありません。これにより、Pimplは、ユーザーがすべての内部変更ですべてのコードを再コンパイルしたくないライブラリの優れたパターンになります。
  • ポリモーフィズムと依存性注入:依存オブジェクトのコードを再コンパイルすることなく、インターフェイスオブジェクトの実装または動作を実行時に簡単に交換できます。単体テストのために何かを模擬する必要がある場合に最適です。

このため、クラシックPimplは3つの部分で構成されています。

  • 実装オブジェクトのインターフェース。これはパブリックでなければならず、インターフェースの仮想メソッドを使用します。

    class IFrobnicateImpl
    {
    public:
        virtual int frobnicate(int) const = 0;
    };
    

    このインターフェースは安定している必要があります。

  • プライベート実装にプロキシするインターフェースオブジェクト。仮想メソッドを使用する必要はありません。許可される唯一のメンバーは、実装へのポインターです。

    class Frobnicate
    {
        std::unique_ptr<IFrobnicateImpl> _impl;
    public:
        explicit Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl = nullptr);
        int frobnicate(int x) const { return _impl->frobnicate(x); }
    };
    
    ...
    
    Frobnicate::Frobnicate(std::unique_ptr<IFrobnicateImpl>&& impl /* = nullptr */)
    : _impl(std::move(impl))
    {
        if (!_impl)
            _impl = std::make_unique<DefaultImplementation>();
    }
    

    このクラスのヘッダーファイルは安定している必要があります。

  • 少なくとも1つの実装

Pimplは、1つのヒープ割り当てと追加の仮想ディスパッチを犠牲にして、ライブラリクラスの安定性を大幅に向上させます。

あなたのソリューションはどのように評価されますか?

  • カプセル化は不要です。メンバーは保護されているため、サブクラスはメンバーを混乱させる可能性があります。
  • インターフェースの安定性がなくなります。データメンバーを変更するときはいつでも(そしてその変更はリファクタリングの1つだけです)、すべての依存コードを再コンパイルする必要があります。
  • これは仮想ディスパッチ層を排除し、実装の簡単な交換を防ぎます。

したがって、Pimplパターンのすべての目的で、この目的を達成できません。したがって、パターンをPimplのバリエーションと呼ぶのは合理的ではなく、はるかに普通のクラスです。実際、メンバー変数はプライベートなので、通常のクラスよりも悪いです。そして、そのキャストは脆弱性の明白なポイントです。

Pimplパターンが常に最適であるとは限らないことに注意してください。一方で安定性と多態性の間でトレードオフがあり、他方でメモリのコンパクト性があります。言語が両方を持つことは意味的に不可能です(JITコンパイルなしで)。したがって、メモリのコンパクト化のためにマイクロ最適化している場合、Pimplは明らかにユースケースに適したソリューションではありません。これらのひどい文字列とベクトルクラスは動的なメモリ割り当てを伴うため、標準ライブラリの半分の使用も停止するでしょう;-)

8
amon

私にとって、利点は欠点を上回らない。

利点

プライベートメソッドのシグネチャのみが変更された場合に再構築を保存するため、コンパイルを高速化できます。ただし、パブリックまたは保護されたメソッドシグネチャまたはプライベートデータメンバーが変更された場合は再構築が必要であり、これらの他のオプションを変更せずにプライベートメソッドシグネチャを変更しなければならないことはまれです。

より積極的なコンパイラの最適化を許可できますが、 [〜#〜] lto [〜#〜] は同じ最適化の多くを許可するはずです(少なくとも、私は可能だと思います-私はコンパイラの最適化ではありません)達人)、それに加えてさらにいくつか、標準および自動にすることができます。

欠点:

あなたはいくつかの欠点を述べました:プライベートを使用できないこと、そしてテンプレートとの複雑さ。しかし、私にとっての最大の欠点は、それが単にぎこちないことです。インターフェイスと実装の間で標準とは異なる標準的なスタイルのジャンプがあり、将来のメンテナや新しいチームメンバーには馴染みがなく、ツールによるサポートが不十分である(例 このGDBバグ を参照)。

最適化に関する標準的な懸念がここに当てはまります。最適化によってパフォーマンスに有意な改善がもたらされることを測定しましたか?これを実行するか、これを維持してホットスポットのプロファイリング、アルゴリズムの改善などに投資するのにかかる時間をとることによって、パフォーマンスは向上しますか?個人的には、ターゲットを絞った最適化を行う時間を空けることを前提として、明確でわかりやすいプログラミングスタイルを選択したいと思います。しかし、それは私が取り組んでいるコードの種類に対する私の見方です-あなたの問題ドメインでは、トレードオフは異なるかもしれません。

補足事項:メソッドのみのpimplでプライベートを許可する

あなたはあなたのメソッドのみの提案でプライベートメンバーを許可する方法について尋ねました。残念ながら、私はメソッドのみの手法を一種のハックと見なしていますが、長所が短所を上回ると判断した場合は、ハックを採用することもできます。

A.h:

#ifndef A_impl
#define A_impl private
#endif

class A
{
public:
    A();
    void DoSomething();
A_impl:
    int mData1;
    int mData2;
};

A.cpp:

#define A_impl public
#include "A.h"
3
Josh Kelley

std::aligned_storageを使用して、インターフェイスクラスでpimplのストレージを宣言できます。

class A
{
std::aligned_storage< 128 > _storage;
public:
  A();
};

実装では、_storageでPimplクラスをインプレースで構築できます。

class Pimpl
{
  int _some_data;
};

A::A()
{
  ::new(&_storage) Pimpl();
  // accessing Pimpl: *(Pimpl*)(_storage);
}

A::~A()
{
  ((Pimpl*)(_storage))->~Pimpl(); // calls destructor for inline pimpl
}
1
Fabio

この興奮を殺すつもりはないが、敬意を払って、コンパイル時の観点からこれが役立つ実用的な利点はないと思います。 pimplsの多くの利点は、ユーザー定義型の詳細を非表示にすることからもたらされます。例えば:

struct Foo
{
    Bar data;
};

...そのような場合、コンパイルの最も重いコストは、Fooを定義するために、Barのサイズ/配置要件を知る必要があるという事実から生じます(つまり、 Bar)の定義が再帰的に必要です。

データメンバーを非表示にしないと、コンパイル時の観点からの最も重要な利点の1つが失われます。また、潜在的に危険に見えるコードがいくつかありますが、ヘッダーは軽量化されておらず、ソースファイルはより多くの転送関数で重くなっているため、全体的にコンパイル時間を短縮するのではなく、増加させる可能性があります。

ライターヘッダーが重要

ビルド時間を短縮するには、ヘッダーを大幅に軽量化するテクニックを示す必要があります(通常、再帰的に#include特定の不要になった詳細を隠したため、他のいくつかのヘッダーstruct/class定義)。本物のpimplsが大きな影響を与える可能性がある場所であり、ヘッダーインクルードのカスケードチェーンを解除し、すべてのプライベートな詳細が隠されたはるかに独立したヘッダーを生成します。

より安全な方法

とにかくこのようなことをしたい場合は、実際にポインターでインスタンス化していない同じクラスを継承するものよりも、ソースファイルで定義されたfriendを使用する方がずっと簡単になります。インスタンス化されていないオブジェクトのメソッドを呼び出すトリックをキャストするか、必要な作業を行うために適切なパラメータを受け取るソースファイル内の内部リンケージを持つ独立した関数を単に使用します(これらのいずれかで、少なくともいくつかのプライベートメソッドを非表示にすることができますコンパイル時間を大幅に節約し、カスケード再コンパイルを回避するために少し小刻みに余裕を持たせるためのヘッダー)。

固定アロケーター

最も安価な種類のpimplが必要な場合、主なトリックは、固定アロケータを使用することです。特に、Pimplを一括で集約する場合、最大のキラーは、空間的な局所性の喪失と、Pimplに初めてアクセスしたときの追加の強制ページフォールトです。メモリをプールするメモリプールを、割り当てられているpimplsに事前に割り当て、メモリを解放する代わりにプールに戻すことにより、pimplインスタンスのボートロードのコストが大幅に削減されます。パフォーマンスの観点からはまだ無料ではありませんが、ずっと安く、はるかにキャッシュ/ページフレンドリーです。

0
user204677

いいえ、パフォーマンスを低下させることなく実装することはできません。 PIMPLは、ランタイムインダイレクションを適用しているため、その性質上、パフォーマンスが低下します。

もちろん、これはあなたが間接的にやりたいwhatに完全に依存します。一部の情報は、消費者によって使用されないだけです。たとえば、4バイト境界で整列された64バイトに正確に入力するものなどです。しかし、他の情報は、オブジェクトに4バイト境界で整列された64バイトが必要であるという事実のようなものです。

パフォーマンスペナルティのない一般的なPIMPLは存在せず、決して存在しません。これは、ユーザーが拒否するのと同じ情報であり、ユーザーが最適化に使用したいものです。あなたが彼らにそれを与えた場合、あなたのIMPLは抽象化されません。それらを拒否すると、最適化できなくなります。あなたはそれを両方の方法で持つことはできません。

0
DeadMG