7

したがって、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 (仮想関数に対する彼の態度など、彼のすべての点に同意しません)で、 MikeActonは次のように述べています。

では、代わりに関数ポインタを使用してみませんか?

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

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

関数ポインタに何らかの隠れたオーバーヘッドがありますか?ストレート関数を呼び出すのは効率的ですか?

4

8 に答える 8

8

この例では、どちらのケースがより高速になるかを言うことは不可能です。推定するには、ターゲット プラットフォーム/コンパイラでこのコードをプロファイリングする必要があります。

一般に、99% の場合、そのようなコードは最適化する必要はありません。これは悪の時期尚早な最適化の例です。人間が読めるコードを書き、プロファイリング後に必要な場合にのみ最適化します。

于 2012-06-19T14:55:09.473 に答える
6

なぜコンパイラにあなたが知っていることを推測させるのですか?

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

プロファイリングが問題であることを示していることを考えるとif、ポインターはコンパイラーが最適化できないレベルの間接性を追加する可能性があるため、ループから抜け出す方が関数ポインターよりも高速であると思います。また、コンパイラが呼び出しをインライン化できる可能性も低くなります。

ただし、ループ内の代わりに抽象インターフェイスを使用する代替設計も検討します。if次に、各データ オブジェクトは、自動的に何をすべきかを既に認識しています。

于 2012-06-19T15:31:55.220 に答える
6

推測しないで、測定してください。

しかし、どうしても推測しなければならないのであれば、3 番目のバリアント (関数ポインター) は 2 番目のバリアント (ifループの外側) よりも遅くなると思います。これは、CPU の分岐予測をより適切に処理する可能性があると思われます。

すでに述べたように、コンパイラがどれほど賢いかによって、最初のバリアントは 2 番目のバリアントと同等である場合と同等でない場合があります。

于 2012-06-19T15:07:13.177 に答える
2

私は、2 番目のバージョンがループの外側で最速になることに賭けif/elseます。ただし、これを最も幅広いコンパイラーで結び付けてテストしたときに払い戻しを受けることを条件とします。:-DI は、VTune を手にしてかなりの年月をかけてこの賭けを行います。

とはいえ、賭けに負けたら、実際には幸せです。最近の多くのコンパイラが最初のバージョンを 2 番目のバージョンに匹敵するように最適化して、ループ内で変化しない変数を繰り返しチェックしていることを検出し、ループの外で分岐を効果的に巻き上げることは非常に可能だと思います。

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

それでも、インライン化に失敗した場合、関数ポインタ ソリューションが最もコストがかかる傾向があります。これは、ロング ジャンプや追加のスタック スピルなどの可能性があるだけでなく、オプティマイザに情報が不足するためです。ポインターを介して呼び出される関数がわからない場合、オプティマイザーの障壁があります。その時点で、IR でこのすべての情報を結合して、命令の選択、レジスタの割り当てなどの最適な仕事を行うことができなくなります。間接関数呼び出しのこのコンパイラ設計の側面は、それほど頻繁に議論されることはありませんが、潜在的に最も高価な部分です。関数を間接的に呼び出す。

于 2018-01-06T12:47:04.493 に答える
1

「非表示」と見なされるかどうかはわかりませんが、もちろん、関数ポインターを使用するには、もう 1 レベルの間接化が必要です。

コンパイラは、通常の関数呼び出しの場合、定数アドレスに直接ジャンプするコードとは対照的に、ポインターを逆参照するコードを生成し、結果のアドレスにジャンプする必要があります。

于 2012-06-19T14:57:48.517 に答える
1

次の 3 つのケースがあります。

ループ内の場合、ループ外の場合、ループ内で関数ポインターを参照解除します。

3 つのうち、WITH NO COMPILER OPTIMIZATION では、3 番目が最適です。1 つ目は条件を実行し、2 つ目は実行したいコードの上でポインターの逆参照を行いますが、3 つ目は目的のコードを実行するだけです。

自分自身を最適化したい場合は、関数ポインター バージョンを実行しないでください。コンパイラが最適化することを信頼していない場合、余分な間接化によってコストがかかる可能性があり、将来誤って壊れやすくなります (私の意見では)。

于 2012-06-19T15:09:12.207 に答える
1

どちらが速いかを測定する必要がありますが、関数ポインターの応答がより高速になるとは思えません。フラグをチェックすると、深い複数のパイプラインを備えた最新のプロセッサでゼロ レイテンシーが発生する可能性があります。関数ポインターを使用すると、コンパイラーが実際の関数呼び出しを強制され、レジスターなどをプッシュする可能性が高くなります。

「なぜコンパイラにあなたが知っていることを推測させるのですか?」

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

于 2012-06-19T16:43:07.247 に答える