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


9

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

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

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

Ahファイル:

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

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

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

更新:

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

UPDATE2:

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

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

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

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


の使用PCALLは未定義の動作です。基になるオブジェクトが実際にtypeでない限り、をにキャストしAPImpl使用することはできませんPImpl。しかし、私が間違っていない限り、ユーザーはタイプのオブジェクトを作成するだけAです。
BeeOnRope 2017年

回答:


8

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は明らかにユースケースに適したソリューションではありません。これらのひどい文字列とベクトルクラスは動的なメモリ割り当てを伴うため、標準ライブラリの半分の使用も停止するでしょう;-)


文字列とベクトルの使用に関する最後のメモについて-それは真実に非常に近いです:)。pimplには私の提案では得られない利点があることを理解しています。ただし、これには利点があります。UPDATEで追加したリンクでより雄弁に表示されます。パフォーマンスが私のユースケースで非常に大きな役割を果たすことを考えると、pimplをまったく使用しないことに対する私の提案をどのように比較しますか?私のユースケースでは、これは私の提案とpimplではないため、pimplには発生したくないパフォーマンスコストがあるため、まったく私の提案とno-pimplです。
dcmm88 2015

1
@ dcmm88パフォーマンスが#1の目標である場合、コードの品質やカプセル化などは明らかに公平なゲームです。ソリューションが通常のクラスよりも優れている唯一のことは、プライベートメソッドのシグネチャが変更されたときに再コンパイルしないことです。私の本では、それほど多くはありませんが、これが大規模なコードベースの基本クラスである場合、奇妙な実装はその価値があるかもしれません。この便利なXKCDチャートを参照して、コンパイル時間の短縮にどれだけの開発時間を費やすことができるかを判断してください。給与によっては、コンピュータのアップグレードの方が安くなる場合があります。
2015

1
また、プライベート関数に内部リンケージを持たせることができ、より積極的な最適化が可能になります。
dcmm88

1
元のクラスのヘッダーファイルでpimplクラスのパブリックインターフェイスを公開しているようです。AFAUIの全体のアイデアは、できるだけ隠すことです。通常、pimplが実装されているのを見る方法では、ヘッダーファイルにはpimplクラスの前方宣言があり、cppにはpimplクラスの完全な定義があります。スワッピング機能には別のデザインパターン、つまりブリッジパターンが付属していると思います。だから、pimplが通常それを提供しないときに、私の提案がそれを提供できないと言うのが公正かどうかはわかりません。
dcmm88

最後のコメントでごめんなさい。私はそれを逃したので、あなたは実際には非常に大規模な答えを持っている、との点は終わりです
BЈовић

3

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

利点

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

より積極的なコンパイラの最適化を許可できますが、LTOは同じ最適化の多くを許可する必要があります(少なくとも、私はコンパイラの最適化の第一人者ではありません)に加えて、さらにいくつかを許可し、標準および自動にすることができます。

短所:

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

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

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

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

ああ:

#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"

私は少し恥ずかしいですが、私はあなたの#define A_implプライベート提案が好きです:) 私も最適化の第一人者ではありませんが、LTOについてある程度の経験はありますが、実際にLTOをいじくり回して、これを思いついたのです。質問について、使用に関する情報と、それでもメソッドのみのpimplが提供する十分なメリットがまだ提供されない理由で更新します。
dcmm88

@ dcmm88-LTOの情報をありがとう。それは私がかなりの経験をしたものではありません。
Josh Kelley

1

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

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

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

class Pimpl
{
  int _some_data;
};

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

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

ストレージサイズをたまに増やす必要があるため、再コンパイルを強制する必要があることを忘れていました。多くの場合、ストレージに少し余裕を持たせれば、再コンパイルをトリガーしない多くの変更を抑制できます。
Fabio

0

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

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

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


私は、パフォーマンスのペナルティはなく、パフォーマンスが向上する可能性があると私が主張する可能性のある「部分的な陰謀」の提案をしました。あなたは私がそれをpimplに関連する何かと呼ぶことに憤慨するかもしれませんが、私は実装の詳細(プライベートメソッド)の一部を隠しています。私はそれをプライベートメソッド実装の非表示と呼ぶこともできましたが、pimplのバリエーション、またはメソッドのみのpimplと呼ぶことにしました。
dcmm88

0

この興奮をなくすつもりはなく、十分に尊重しているので、これがコンパイル時の観点から提供する実際的な利点はないと思います。の多くの利点は、pimplsユーザー定義型の詳細を非表示にすることから得られます。例えば:

struct Foo
{
    Bar data;
};

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

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

ライターのヘッダーが鍵

ビルド時間を短縮するには、ヘッダーを大幅に軽量化する手法を示す必要があります(通常、#include特定のstruct/class定義を必要としない詳細を非表示にしたため、他のヘッダーを再帰的に使用できなくなるため)。これが本物pimplsが大きな効果を発揮できる場所であり、ヘッダーインクルードのカスケードチェーンを切り離し、すべてのプライベートな詳細が隠されたはるかに独立したヘッダーを生成します。

より安全な方法

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

固定アロケーター

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

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.