30

ずっと前にアドバイスされたように、私は常にフレーム ポインターを使用せずにリリース実行可能ファイルをビルドします (/Ox でコンパイルする場合の既定値です)。

ただし、今、論文http://research.microsoft.com/apps/pubs/default.aspx?id=81176を読みましたが、フレーム ポインタはパフォーマンスにあまり影響を与えません。そのため、完全に最適化 (/Ox を使用) するか、フレーム ポインターを使用して完全に最適化 (/Ox /Oy- を使用) しても、実際にはパフォーマンスに違いはありません。

Microsoft は、フレーム ポインター (/Oy-) を追加するとデバッグが容易になると指摘しているようですが、これは本当ですか?

私はいくつかの実験を行い、次のことに気付きました。

  • 単純な 32 ビット テスト実行可能ファイル (/Ox /Ob0 を使用してコンパイル) では、フレーム ポインターを省略するとパフォーマンスが向上します (約 10%)。ただし、このテスト実行可能ファイルは、いくつかの関数呼び出しのみを実行し、他には何も実行しません。
  • 私自身のアプリケーションでは、フレーム ポインターの追加/削除は大きな影響を与えないようです。フレーム ポインターを追加すると、アプリケーションは約 5% 高速になるようですが、誤差の範囲内である可能性があります。

フレームポインタに関する一般的なアドバイスは何ですか?

  • それらは実際にパフォーマンスに良い影響を与えるため、リリース実行可能ファイルでは省略 (/Ox) する必要がありますか?
  • デバッグ機能を向上させるため (クラッシュ ダンプ ファイルを使用してデバッグする場合)、リリース実行可能ファイルに追加 (/Ox /Oy-) する必要がありますか?

Visual Studio 2010 を使用しています。

4

1 に答える 1

43

簡単な答え:フレーム ポインターを省略すると、

ローカル変数と引数にアクセスするには、スタック ポインターを使用する必要があります。コンパイラは気にしませんが、アセンバーでコーディングしている場合、これはあなたの人生を少し難しくします。マクロを使わないと大変です。

関数呼び出しごとに 4 バイト (32 ビット アーキテクチャ) のスタック領域を節約できます。深い再帰を使用していない限り、これは勝利ではありません。

キャッシュされたメモリ (スタック) へのメモリ書き込みを保存し、(理論的には) 関数のエントリ/終了時に数クロック ティックを保存しますが、コード サイズを増やすことができます。関数が非常に頻繁にほとんど実行しない限り (その場合はインライン化する必要があります)、これは目立たないはずです。

汎用レジスターを解放します。コンパイラがレジスタを利用できる場合、大幅に小さく、潜在的に高速なコードが生成されます。ただし、CPU 時間のほとんどがメイン メモリ (またはハード ドライブ) との通信に費やされている場合、フレーム ポインターを省略しても、それを回避することはできません。

デバッガーは、スタック トレースを生成する簡単な方法を失います。デバッガーは、別のソース ( PDB ファイルなど) からスタック トレースを生成できる場合があります。


長い答え:

典型的な関数の入口と出口は次のとおりです。

PUSH SP   ;push the frame pointer
MOV FP,SP ;store the stack pointer in the frame pointer
SUB SP,xx ;allocate space for local variables et al.
...
LEAVE     ;restore the stack pointer and pop the old frame pointer
RET       ;return from the function

スタック ポインターのないエントリとエグジットは次のようになります。

SUB SP,xx ;allocate space for local variables et al.
...
ADD SP,xx ;de-allocate space for local variables et al.
RET       ;return from the function.

2 つの命令を保存しますが、コードが短くならないようにリテラル値も複製します (まったく逆です)。ただし、数クロック サイクルを節約できた可能性があります (または、命令キャッシュでキャッシュ ミスが発生した場合)。 . ただし、スタックのスペースをいくらか節約できました。


汎用レジスターを解放します。これはメリットしかありません。

regcall/fastcall では、これは関数に引数を格納できる 1 つの追加レジスタです。したがって、関数が 7 つ (x86 の場合。他のほとんどのアーキテクチャではそれ以上) またはそれ以上の引数 ( を含むthis) を取る場合でも、7 番目の引数はレジスタに収まります。さらに重要なことに、同じことがローカル変数にも当てはまります。配列と大きなオブジェクトはレジスタに収まりません (ただし、それらへのポインターは収まります)。ただし、関数が 7 つの異なるローカル変数 (複雑な式を計算するために必要な一時変数を含む) を使用している場合、コンパイラはより小さなコードを生成できる可能性があります。 . コードが小さいということは、命令キャッシュのフットプリントが小さいことを意味します。つまり、ミス率が低下し、メモリ アクセスがさらに少なくなります (ただし、Intel Atom には 32K の命令キャッシュがあるため、コードはおそらく収まります)。

x86 アーキテクチャには、[BX/BP/SI/DI]および[BX/BP + SI/DI]アドレッシング モードがあります。これにより、特に配列ポインターが SI または DI レジスターに存在する場合、BP レジスターはスケーリングされた配列インデックスの非常に便利な場所になります。オフセット レジスタは 2 つの方が 1 つよりも優れています。

レジスタを利用するとメモリ アクセスが回避されますが、変数をレジスタに格納する価値がある場合は、L1 キャッシュでも問題なく存続する可能性があります (特に、スタック上にあるため)。キャッシュとの間の移動にはまだコストがかかりますが、最新の CPU は多くの移動の最適化と並列化を行うため、L1 アクセスがレジスタ アクセスと同じくらい高速になる可能性があります。したがって、データを移動しないことによる速度の利点は依然として存在しますが、それほど大きくはありません。少なくとも読み取りに関する限り、CPU がデータ キャッシュを完全に回避していることは容易に想像できます (キャッシュへの書き込みは並行して実行できます)。

利用されるレジスターは、保存が必要なレジスターです。再度使用する前にとにかくスタックにプッシュする場合は、レジスタに多くを格納する価値はありません。呼び出し元ごとに保持する呼び出し規約 (上記のものなど) では、これは永続ストレージとしてのレジスタが、他の関数を頻繁に呼び出す関数ではあまり役に立たないことを意味します。

また、x86 には浮動小数点レジスタ用の個別のレジスタ空間があることにも注意してください。つまり、浮動小数点数は追加のデータ移動命令がなければ BP レジスタを利用できません。整数とメモリ ポインタのみが対象です。


フレーム ポインターを省略することで失われるのは、デバッグ可能性です。この回答は、その理由を示しています。

コードがクラッシュした場合、スタック トレースを生成するためにデバッガーが行う必要があるのは次のとおりです。

    PUSH FP      ; log the current frame pointer as well
$1: CALL log_FP  ; log the frame pointer currently on stack
    LEAVE        ; pop the frame pointer to get the next one
    CMP [FP+4],0
    JNZ $1       ; until the stack cannot be popped (the return address is some specific value)

フレーム ポインターなしでコードがクラッシュした場合、デバッガーは、スタック ポインターからどれだけ減算する必要があるかわからない (つまり、関数の入口/出口ポイントを見つける必要がある) ため、スタック トレースを生成する方法がない可能性があります。フレーム ポインターが使用されていないことをデバッガーが認識しない場合、デバッガー自体がクラッシュすることさえあります。

于 2012-10-22T08:06:19.073 に答える