2012-06-19 5 views
6

ので、このルールは、高繰り返しのうち、if文を引くしようとするがありますループ:関数ポインタを使用して文をより効率的にする場合は、カットしていますか?

for(int i = 0 ; i < 10000 ; i++) 
{ 
    if(someModeSettingOn) doThis(data[i]) ; 
    else doThat(data[i]) ; 
} 

彼らはそれが外にif文を入れて、それを分割すると良いでしょう、と言う:

if(someModeSettingOn) 
    for(int i = 0 ; i < 10000 ; i++) 
    doThis(data[i]) ; 
else 
    for(int i = 0 ; i < 10000 ; i++) 
    doThat(data[i]) ;  

(もしあなたが "Ho!自分自身を最適化しないでください!コンパイラはそれをやります!"と言っているでしょうか?)もちろん、オプティマイザがこれを行うかもしれません。しかし、Typical C++ Bullshit(私はと同義ではありません。彼のポイント、例えば仮想関数に対する彼の態度)Mike Actonは "なぜコンパイラがあなたに何かを推測させるのですか?を知っていますか? 。

なぜ代わりに、関数ポインタを使用していない?

FunctionPointer *fp ; 
if(someModeSettingOn) fp = func1 ; 
else fp = func2 ; 

for(int i = 0 ; i < 10000 ; i++) 
{ 
    fp(data[i]) ; 
} 

をポインタを機能させるに隠されたオーバーヘッドのいくつかの種類がありますか?それはまっすぐ関数を呼び出すように効率的ですか?

+12

あなたは自分の状況でそれをプロファイルし、参照する必要があります。分岐予測は、一貫した 'if'ステートメントのオーバーヘッドを減らすために大きく貢献しました。 –

答えて

8

この例ではそれはです不可能この場合はより高速になります。このコードをターゲットプラットフォーム/コンパイラでプロファイルして推定する必要があります。 99%の場合、そのようなコードに

、一般的には、最適化される必要はありません。これは悪い早期の最適化の例です。 人が読めるコードを記述し、プロファイリング後に必要な場合にのみ最適化します。それは「隠された」としての資格が、関数ポインタを使用して、もちろん間接の1つの以上のレベルが必要な場合はわからない

1

コンパイラは、ポインタ間接参照にコードを生成し、その後、ちょうど直接通常の関数コールのために、一定のアドレスにジャンプするコードとは対照的に、得られたアドレスにジャンプしなければなりません。

+1

"通常の関数呼び出しのために定数アドレスに直接ジャンプするコード" - これは通常の関数呼び出しの*最悪の場合です。インライン化されている可能性があります。 –

+1

あなたは、関数ポインタの使用において、必ずしも「もう一つのレベルのインダイレクション」があるとは限りません。何らかの 'call '関数(動的リンカーによって定数が固定されている)と比較して、何らかの 'call '命令です。あるいは 'set 'と 'call 'と思われるかもしれません。私は 'x + y'は' x + 12345'と比較して余分なレベルの間接的な*を含んでいると一般には思われません。 stuffが積まれると、変数を使う余分な仕事があるかもしれませんが、一般に関数ポインタの実際の理由はこれより遅いのです。 –

6

は、対策を推測しないでください。

しかし、私は絶対に推測しがあった場合、私は第三の変形(関数ポインタ)が、私はCPUの分岐予測で遊ぶかもしれない疑いれ、第二の変形(if外ループ)よりも遅くなるだろうと言うだろうより良い。すでに指摘しているよう

第一の変形はまたは、コンパイラがどのようにスマートに応じて、第二と同等であってもなくてもよいです。

1

あなたは3例があります:ループの内側場合はループの外であれば、ループ内で関数ポインタデリファレンスを

を。 3の

は、NOコンパイラの最適化では、第三は最高になるだろう。最初のコードは条件付きで、2番目のコードは実行したいコードの上にポインタの参照を外します.3番目のコードは、あなたが望むコードを実行します。

自分自身を最適化したい場合は、関数ポインタバージョンを使用しないでください!コンパイラが最適化するのを信頼しないと、余計な間接化によってコストがかさむことになり、将来私が誤って壊れる可能性は非常に高くなります。

+0

質問と同じ3つのケースを注文した場合、あなたの答えはより強くなりました。そして、ループの外側にある「もし」は、最適化がなくてもより高速になることが保証されていると私は同意しない。私はそれが遅くなる可能性は低いことに同意する。 –

+0

@ MarkRansom - 興味深い。それが遅くならない時のあなたの考えは何ですか?私が思いつくことができるのは、CPUが分岐予測をしていて、ループの過程でCPUを失うことがないということだけです。 –

+1

分岐予測だけではなく、多くのループはメモリ帯域幅の制約があるため、最後のサイクルをすべて最適化しても効果はありません。あなたが命令を見て、それが取るだろうクロックの正確な数を知ることができた時代は、どうして私は恋しいのですか? OK、そうではないかもしれない。 –

6

なぜコンパイラはあなたが知っているものを推測するのですか?

コードのユーザーに明白なメリットを提供することなく、将来のメンテナーのためにコードを複雑にする可能性があるためです。この変更は、時期尚早の最適化が強く、プロファイリングの後で明白な(ifループ内の)実装以外のものを検討することになります。

プロファイリングが問題であることがわかっているとすれば、ifをループから抜くのは、ポインタがコンパイラが最適化できない間接レベルを追加する可能性があるためです。 。また、コンパイラがすべての呼び出しをインライン化できる可能性が低くなります。

ループ内のifではなく抽象的なインターフェイスを使用した別のデザインを検討してください。次に、各データオブジェクトはすでに自動的に何をすべきかを知っています。

+1

ポインタのインダイレクションは傷つくだけでなく、関数呼び出し自体がいくつかの最適化を妨げる可能性があります。 –

1

どちらが高速であるかを測定する必要がありますが、関数ポインタの応答がより高速になることは非常に疑問です。フラグprobalbyをチェックすると、複数のパイプラインが深い現代のプロセッサでは、レイテンシがゼロになります。関数ポインタを使用すると、コンパイラは実際の関数呼び出し、レジスタのプッシュなどを余儀なくされる可能性が高くなります。

"なぜコンパイラはあなたが知っているものを推測するのですか?"

あなたとコンパイラの両方がコンパイル時にいくつかのことを知っていますが、プロセッサは実行時にさらに多くのことを知っています - その内部ループに空のパイプラインがあるようです。この種の最適化を行った日は、組み込みシステムやグラフィックスシェイダーの外に出ています。

2

if/elseがループの外で最も速くなるように私の賭けをするのは、幅広い範囲のコンパイラを結んでこれをテストするときに払い戻しを受けるということです。私はかなりの年数でこの賭けをしています。

私が賭けを失った場合、私は実際に幸せになると言っていました。現在、多くのコンパイラが、第2バージョンに匹敵するように最初のバージョンを最適化して、ループ内で変化しない変数を繰り返しチェックしているので、ループの外側で分岐を効果的に持ち上げることができます。

しかし、私は、オプティマイザが間接関数呼び出しをインライン化するのと同等のことを行っているのを見たことがありません...オプティマイザがこれを行う可能性がある場合は、間違いなく最も簡単なのは、関数ポインターを介してそれらの関数を呼び出すのと同じ関数を呼び出す関数にアドレスを割り当てるからです。オプティマイザがこれを行うことができれば、特に保守性の観点から、3番目のバージョンが一番好きなので、本当にうれしく思います。(たとえば、呼び出す関数が異なる新しい条件を追加する場合は、変更するのが最も簡単です。

インライン化に失敗した場合でも、関数ポインタソリューションは、ジャンプが長くなり、スタックの追加が発生するなどの理由で最もコストがかかる傾向があります。情報が不足している - ポインタを介して呼び出される関数がわからないときにオプティマイザの障壁が存在する。その時点で、IRでこの情報をすべて集約することはできなくなり、命令の選択、レジスタの割り当てなどの最善の仕事をすることができなくなります。間接関数呼び出しのこのコンパイラ設計の側面はあまり頻繁に議論されませんが、間接的に関数を呼び出す

1

他のすべては非常に有効なポイントを上げています。特に測定する必要があります。私は3つのことを追加したい:

  1. 1つの重要な側面は、関数ポインタを使用すると、多くの場合、あなたのコードのパフォーマンスを殺すことができるインライン化を防ぐことです。しかし、それは間違いなく依存しています。 godboltコンパイラエクスプローラで遊んしようとすると、生成されたアセンブリを見て:

    https://godbolt.org/g/85ZzpK

    doThisdoThatは、例えば、定義されていないときよりも、 DSO境界を越えて起こる可能性があるため、大きな違いはありません。

  2. 第2の点は、分岐予測に関連しています。 https://danluu.com/branch-prediction/をご覧ください。ここにあるコードは、実際には分岐予測子にとって理想的なケースであることを明確にする必要があります。したがって、おそらく気にする必要はありません。再度、perfやVTuneのような良いプロファイラは、分岐の予測ミスに苦しんでいるかどうかを教えてくれます。

  3. 最後に、上記の推論にもかかわらず、ループを構成する条件文を引き上げることで大きな違いが見られた少なくとも1つのシナリオがありました。これはタイトな数学的ループであり、条件付きのために自動ベクトル化されませんでした。 GCCとClangは、どのループがベクトル化されるのか、それがなぜ行われなかったのかに関するレポートを出力できます。私のケースでは、条件は実際にオートベクトルの問題でした。これはGCC 4.8で行われていたので、それ以降は変更されている可能性があります。 Godboltでは、これがあなたにとって問題であるかどうかを確認するのはかなり簡単です。繰り返しますが、常にターゲットマシンで測定し、影響を受けているかどうかを確認してください。

関連する問題