14

次のような状況があります。


class A
{
public:
    A(int whichFoo);
    int foo1();
    int foo2();
    int foo3();
    int callFoo(); // cals one of the foo's depending on the value of whichFoo
};

私の現在の実装whichFooでは、コンストラクターのデータ メンバーに inの値を保存し、 switchinを使用してcallFoo()、どの foo を呼び出すかを決定します。または、コンストラクターでa を使用して、で呼び出されるswitch右側へのポインターを保存することもできます。fooN()callFoo()

私の質問は、クラス A のオブジェクトが一度だけ構築さcallFoo()れ、非常に多数回呼び出される場合、どの方法がより効率的かということです。したがって、最初のケースでは、switch ステートメントが複数回実行されますが、2 番目のケースでは、switch が 1 つしかなく、そのポインターを使用してメンバー関数が複数回呼び出されます。ポインターを使用してメンバー関数を呼び出すと、直接呼び出すよりも遅いことがわかっています。このオーバーヘッドが のコストより多いか少ないかは誰にもわかりswitchませんか?

明確化: どのアプローチがパフォーマンスを向上させるかは、実際に試して時間を計るまでわからないことを認識しています。ただし、この場合、既にアプローチ 1 を実装しており、アプローチ 2 が少なくとも原則としてより効率的であるかどうかを調べたいと考えていました。それが可能であるように思われ、今ではわざわざそれを実装して試してみるのは理にかなっています.

ああ、私は審美的な理由からアプローチ 2 の方が好きです。私はそれを実装する正当な理由を探していると思います。:)

4

12 に答える 12

12

ポインターを介してメンバー関数を呼び出すと、直接呼び出すよりも遅いと確信していますか? 違いを測定できますか?

一般に、パフォーマンス評価を行うときは、直感に頼るべきではありません。コンパイラとタイミング関数を用意して、さまざまな選択肢を実際に測定してください。あなたは驚くかもしれません!

詳細情報: 優れた記事Member Function Pointers and the Fastest possible C++ Delegatesがあり、メンバー関数ポインターの実装について非常に詳細に説明されています。

于 2008-09-22T04:18:45.237 に答える
9

これを書くことができます:

class Foo {
public:
  Foo() {
    calls[0] = &Foo::call0;
    calls[1] = &Foo::call1;
    calls[2] = &Foo::call2;
    calls[3] = &Foo::call3;
  }
  void call(int number, int arg) {
    assert(number < 4);
    (this->*(calls[number]))(arg);
  }
  void call0(int arg) {
    cout<<"call0("<<arg<<")\n";
  }
  void call1(int arg) {
    cout<<"call1("<<arg<<")\n";
  }
  void call2(int arg) {
    cout<<"call2("<<arg<<")\n";
  }
  void call3(int arg) {
    cout<<"call3("<<arg<<")\n";
  }
private:
  FooCall calls[4];
};

実際の関数ポインターの計算は線形で高速です。

  (this->*(calls[number]))(arg);
004142E7  mov         esi,esp 
004142E9  mov         eax,dword ptr [arg] 
004142EC  push        eax  
004142ED  mov         edx,dword ptr [number] 
004142F0  mov         eax,dword ptr [this] 
004142F3  mov         ecx,dword ptr [this] 
004142F6  mov         edx,dword ptr [eax+edx*4] 
004142F9  call        edx 

コンストラクターで実際の関数番号を修正する必要さえないことに注意してください。

このコードを a によって生成された asm と比較しましたswitch。このswitchバージョンでは、パフォーマンスは向上しません。

于 2008-09-22T05:39:07.527 に答える
2

尋ねられた質問に答えるには、最も細かいレベルでは、メンバー関数へのポインターのパフォーマンスが向上します。

ここで「より良い」とはどういう意味ですか? ほとんどの場合、違いはごくわずかだと思います。ただし、クラスの内容によっては、違いが大きくなる場合があります。違いを気にする前にパフォーマンス テストを行うことは、明らかに正しい最初のステップです。

于 2008-09-22T04:22:22.797 に答える
2

スイッチを引き続き使用する場合は、まったく問題ありませんが、おそらくロジックをヘルパー メソッドに配置し、コンストラクターから if を呼び出す必要があります。あるいは、これは戦略パターンの典型的なケースです。Foo の署名を持つ 1 つのメソッドを持つ IFoo という名前のインターフェイス (または抽象クラス) を作成できます。コンストラクターに IFoo (必要な foo メソッドを実装したコンストラクター依存性注入) のインスタンスを取り込むようにします。このコンストラクターで設定されるプライベート IFoo があり、Foo を呼び出すたびに、 IFooのバージョン。

注: 私は大学時代から C++ を扱っていないので、ほとんどの OO 言語に共通する一般的な考え方とは異なり、私の専門用語はここから外れている可能性があります。

于 2008-09-22T04:25:15.737 に答える
2

あなたの例が実際のコードである場合は、クラスの設計を再検討する必要があると思います。値をコンストラクターに渡し、それを使用して動作を変更することは、実際にはサブクラスを作成することと同じです。より明確にするためにリファクタリングを検討してください。そうすることの効果は、コードが関数ポインターを使用することになります (すべての仮想メソッドは、実際には、ジャンプ テーブル内の関数ポインターです)。

ただし、コードが、一般的にジャンプ テーブルが switch ステートメントよりも高速かどうかを尋ねる単純な例である場合、私の直感では、ジャンプ テーブルの方が高速であると言えますが、コンパイラの最適化ステップに依存しています。しかし、パフォーマンスが本当に問題になる場合は、直感に頼らないでください。テスト プログラムを作成してテストするか、生成されたアセンブラを確認してください。

1 つ確かなことは、switch ステートメントがジャンプ テーブルよりも遅くなることはないということです。その理由は、コンパイラのオプティマイザーができる最善のことは、一連の条件付きテスト (つまり、スイッチ) をジャンプ テーブルに変えることです。したがって、本当に確実にしたい場合は、コンパイラを決定プロセスから除外し、ジャンプ テーブルを使用します。

于 2008-09-22T07:01:26.173 に答える
1

タイマーを使用して、どちらが速いかを確認します。ただし、このコードが何度も繰り返される場合を除いて、違いに気付く可能性はほとんどありません。

コンストラクターからコードを実行している場合は、コンストラクターが失敗してもメモリがリークしないことを確認してください。

この手法は、Symbian OSで頻繁に使用されます: http ://www.titu.jyu.fi/modpa/Patterns/pattern-TwoPhaseConstruction.html

于 2008-09-22T13:30:35.347 に答える
1

callFoo()を1回だけ呼び出す場合、ほとんどの場合、関数ポインターはわずかな量だけ遅くなります。ほとんどの場合よりも何度も呼び出している場合、関数ポインターはわずかな量だけ高速になります(スイッチを通過し続ける必要がないため)。

いずれにせよ、アセンブルされたコードを見て、それがあなたがしていると思うことをしていることを確認してください。

于 2008-09-22T13:38:14.410 に答える
1

見過ごされがちな切り替えの利点の 1 つは (並べ替えやインデックス作成よりも優れている場合でも)、特定の値がほとんどの場合に使用されることがわかっている場合です。最も一般的なものが最初にチェックされるように、スイッチを注文するのは簡単です。

ps。グレッグの答えを補強するために、速度が気になる場合は測定してください。CPU にプリフェッチ / 予測分岐やパイプライン ストールなどがある場合、アセンブラを調べても役に立ちません。

于 2008-09-22T13:48:04.400 に答える
1

ほとんどの場合、関数ポインターは、連鎖された if よりも優れています。それらはよりクリーンなコードを作成し、ほぼ常に高速です (おそらく、2 つの関数から選択するだけで、常に正しく予測される場合を除きます)。

于 2008-09-22T04:24:53.727 に答える
1

ポインターの方が速いと思います。

最新の CPU は命令をプリフェッチします。予測を誤ったブランチはキャッシュをフラッシュします。つまり、キャッシュを補充している間、ブランチは停止します。ポインターはそれをしません。

もちろん、両方を測定する必要があります。

于 2008-09-22T04:25:44.020 に答える
1

必要なときだけ最適化する

最初に: ほとんどの場合、気にする必要はありませんが、違いは非常に小さいものです。最初に、この呼び出しを最適化することが本当に理にかなっていることを確認してください。呼び出しのオーバーヘッドにかなりの時間が費やされていることを測定結果が示している場合にのみ、最適化に進みます (恥知らずなプラグイン -アプリケーションを最適化して高速化する方法を参照)。最適化が重要でない場合は、より読みやすいコードを優先してください。

間接呼び出しのコストはターゲット プラットフォームによって異なります

低レベルの最適化を適用する価値があると判断したら、ターゲット プラットフォームを理解します。ここで回避できるコストは、分岐予測ミスのペナルティです。最新の x86/x64 CPU では、この予測ミスは非常に小さい可能性があります (ほとんどの場合、間接呼び出しを非常にうまく予測できます) が、PowerPC またはその他の RISC プラットフォームを対象とする場合、間接呼び出し/ジャンプはまったく予測されず、回避されることがよくあります。パフォーマンスが大幅に向上する可能性があります。仮想通話料金はプラットフォームによって異なりますも参照してください。

コンパイラは、ジャンプ テーブルを使用してスイッチを実装することもできます

1 つの落とし穴: Switch は、(テーブルを使用した) 間接呼び出しとしても実装できる場合があります。特に、多くの可能な値を切り替える場合はそうです。このようなスイッチは、仮想機能と同じ予測ミスを示します。この最適化の信頼性を高めるには、最も一般的なケースで switch の代わりに if を使用することをお勧めします。

于 2008-09-22T09:20:39.317 に答える
1

callFoo純粋な仮想関数を作成し、のサブクラスをいくつか作成する必要があるように聞こえますA

速度が本当に必要な場合を除き、大規模なプロファイリングと計測を行い、 の呼び出しcallFooが本当にボトルネックであると判断します。ありますか?

于 2008-09-22T04:19:42.833 に答える