C ++ CPUレジスタの使用
-
11-09-2019 - |
質問
C ++では、ローカル変数は常にスタックに割り当てられます。スタックは、アプリケーションが占有できる許可されたメモリの一部です。そのメモリはRAMに保持されます(ディスクに交換されない場合)。これで、C ++コンパイラは常にスタックにローカル変数を保存するアセンブラコードを作成しますか?
たとえば、次の簡単なコードを考えてみましょう。
int foo( int n ) {
return ++n;
}
MIPSアセンブラーコードでは、これは次のようになります。
foo:
addi $v0, $a0, 1
jr $ra
ご覧のとおり、nにスタックをまったく使用する必要はありませんでした。 C ++コンパイラは、CPUのレジスタを直接使用しますか?
編集: うわー、あなたのほぼ即時で広範な答えをありがとう!もちろん、fooの関数本文はそうです return ++n;
, 、 いいえ return n++;
. :)
解決
免責事項:私はMIPSを知りませんが、私はいくつかのx86を知っています、そして私は原則が同じであるべきだと思います。
通常の機能コール条約では、コンパイラはの値をプッシュします n
スタックに関数に渡します foo
. 。ただし、があります fastcall
代わりにレジスタを介して値を渡すようにGCCに指示できる条約。 (MSVCにはこのオプションもありますが、その構文が何であるかはわかりません。)
test.cpp:
int foo1 (int n) { return ++n; }
int foo2 (int n) __attribute__((fastcall));
int foo2 (int n) {
return ++n;
}
上記のコンパイル g++ -O3 -fomit-frame-pointer -c test.cpp
, 、私は手に入れます foo1
:
mov eax,DWORD PTR [esp+0x4]
add eax,0x1
ret
ご覧のとおり、スタックから値が表示されます。
そして、これがあります foo2
:
lea eax,[ecx+0x1]
ret
これで、レジスタから直接値を取得します。
もちろん、関数にインラインで、コンパイラは、指定する呼び出し条約に関係なく、より大きな関数の本文で簡単な追加を行います。しかし、あなたがそれをインラインにすることができないとき、これは起こるでしょう。
免責事項2:コンパイラを継続的に2番目に推測すべきだと言っているわけではありません。それはおそらく実用的ではなく、ほとんどの場合に必要ではありません。しかし、それが完全なコードを生成すると仮定しないでください。
編集1: 単純なローカル変数(関数引数ではない)について話している場合、はい、コンパイラはそれらをレジスタまたはスタックに適合したときに割り当てます。
編集2: リチャード・ペニントンが彼の答えで述べているように、Calling Conventionはアーキテクチャ固有であり、MIPSはスタックで最初の4つの引数を渡すと思われます。したがって、あなたの場合、追加の属性を指定する必要はありません(実際にはx86固有の属性です。)。
他のヒント
はい。 「変数は常にスタックに割り当てられている」というルールはありません。 C ++標準には、スタックについては何も述べていません。スタックが存在すること、またはレジスタが存在するとは想定していません。コードの実装方法ではなく、どのように動作するかを示しています。
コンパイラは、必要なときに変数のみをスタックに保存します - たとえば、機能コールを通過する必要がある場合、またはそれらのアドレスを取得しようとする場合。
コンパイラは愚かではありません。 ;)
はい、優れた、最適化C/C ++が最適化します。そしてさえ 多くの もっと: こちらを参照してください:Felix von Leitners Compiler Survey.
通常のC/C ++コンパイラは、とにかくすべての変数をスタックに置くとは限りません。あなたの問題 foo()
関数は、変数がスタックを介して関数(システムのABI(ハードウェア/OS)が定義する)を介して渡される可能性がある可能性があります。
Cで register
キーワードコンパイラを与えることができます ヒント 変数をレジスタに保存するのはおそらく良いことです。サンプル:
register int x = 10;
ただし、覚えておいてください:コンパイラは無料で保存しないでください x
必要に応じてレジスタで!
答えはそうです、多分。コンパイラ、最適化レベル、およびターゲットプロセッサに依存します。
MIPSの場合、最初の4つのパラメーターは、小さい場合、レジスタで渡され、返品値がレジスタで返されます。したがって、例には、スタックに何かを割り当てる必要はありません。
実際、真実はフィクションよりも奇妙です。あなたの場合、パラメーターは変更されずに返されます。返される値は、++演算子の前のnの値です。
foo:
.frame $sp,0,$ra
.mask 0x00000000,0
.fmask 0x00000000,0
addu $2, $zero, $4
jr $ra
nop
あなたの例から foo
関数はID関数(引数を返すだけです)であり、私のC ++コンパイラ(VS 2008)はこの関数呼び出しを完全に削除します。私がそれを変更する場合:
int foo( int n ) {
return ++n;
}
コンパイラにはこれを挿入します
lea edx, [eax+1]
はい、レジスタはC ++で使用されます。 MDR(メモリデータレジスタ)には、フェッチおよび保存されているデータが含まれています。たとえば、Cell 123の内容を取得するには、値123(バイナリ)をMARにロードし、フェッチ操作を実行します。操作が完了すると、セル123の内容のコピーがMDRにあります。値98をCell 4に保存するには、MARに4を、98をMDRにロードしてストアを実行します。操作が完了すると、以前にあったものを破棄することにより、操作が98に設定されます。データとアドレスのレジスタは、これを達成するためにそれらと連携します。 C ++でも、VARを最初に値で初期化したり、その値を尋ねたりすると、同じ現象が発生します。
そして、もう1つ、最新のコンパイラもレジスタ割り当てを実行します。これは、メモリ割り当てよりも速いです。