isocpp FAQ のinline
に関するこの質問を読んでいます。コードは
void f()
{
int x = /*...*/;
int y = /*...*/;
int z = /*...*/;
// ...code that uses x, y and z...
g(x, y, z);
// ...more code that uses x, y and z...
}
それからそれは言う
レジスターとスタックを持つ典型的なC++実装を想定すると、レジスターとパラメーターは
g()
の呼び出しの直前にスタックに書き込まれ、その後パラメーターはg()
内のスタックから読み取られて読み取られますg()
がf()
に戻る間、レジスタを再び復元します。しかし、特にコンパイラが変数x
、y
およびz
にレジスタを使用できる場合は、これは多くの不要な読み取りと書き込みです。各変数が2回書き込まれる可能性があります。 (レジスタとして、またパラメータとして)、2回読み取ります(g()
内で使用され、f()
に戻るときにレジスタを復元するため)。
上記の段落を理解するのが非常に困難です。私は私の質問を以下のようにリストしようとします:
f()
はg(x, y, z)
が関数であるのと同じように関数だと思います。 g()
を呼び出す前のx, y, z
がレジスタにあり、g()
で渡されたパラメータがスタックにあるのはなぜですか?x, y, z
の宣言により、それらがレジスタに格納されることがどのようにしてわかりますか? g()
内のデータはどこに保存、登録、またはスタックされますか?[〜#〜] ps [〜#〜]
答えがすべて非常に良い場合(たとえば、@ MatsPeterson、@ TheodorosChatzigiannakis、および@superultranovaによって提供されるもの)、許容できる答えを選択するのは非常に難しいと思います。 @Potatoswatterの方が個人的に気に入っています。回答にはいくつかのガイドラインがあるからです。
その段落をあまり真剣に受け取らないでください。それは過度の仮定を行ってから、過度に詳細になっているようですが、これは実際に一般化することはできません。
しかし、あなたの質問はとても良いです。
- コンピュータがメインメモリにある一部のデータに対していくつかの操作を行うには、データをいくつかのレジスタに最初にロードしてから、CPUがそのデータを操作できるというのは本当ですか? (私はこの質問がC++に特に関連していないことを知っていますが、これを理解することはC++がどのように機能するかを理解するのに役立ちます。)
多かれ少なかれ、すべてがレジスタにロードされる必要があります。ほとんどのコンピューターは、レジスタ、演算回路、およびメモリ階層の最上位を接続するバスであるdatapathを中心に編成されています。通常、データパスでブロードキャストされるものはすべてレジスタで識別されます。
RISC対CISCの大きな議論を思い出すかもしれません。重要な点の1つは、メモリを直接演算回路に接続できないようにすると、コンピュータの設計がはるかに簡単になることです。
最近のコンピュータには、変数のようなプログラミング構造であるアーキテクチャレジスタと、実際の回路である物理レジスタがあります。コンパイラーは、アーキテクチャー・レジスターの観点からプログラムを生成している間、物理レジスターを追跡するために多くの重労働を行います。 x86のようなCISC命令セットの場合、これには、メモリ内のオペランドを直接算術演算に送信する命令の生成が含まれる場合があります。しかし、舞台裏では、それはずっと下で登録されています。
結論:コンパイラーに処理を任せます。
- f()は、g(x、y、z)が関数であるのと同じように関数です。g()はレジスターにあり、g()で渡されるパラメーターはスタックにありますか?
各プラットフォームは、C関数が相互に呼び出す方法を定義します。レジスターでパラメーターを渡す方が効率的です。ただし、トレードオフがあり、レジスターの総数は制限されています。古いABIは、単純化のために効率を犠牲にし、それらすべてをスタックに入れます。
結論:この例は、単純に単純なABIを想定しています。
- X、y、zの宣言により、それらがレジスタに格納されることがどのようにしてわかりますか? g()内のデータはどこに保存、登録、またはスタックされますか?
コンパイラーは、より頻繁にアクセスされる値にはレジスターを使用する傾向があります。この例では、スタックを使用する必要はありません。ただし、アクセス頻度の低い値がスタックに配置され、より多くのレジスターが使用可能になります。
&x
や参照渡しなどによって変数のアドレスを取得し、そのアドレスがインライナーをエスケープする場合のみ、コンパイラーはレジスターではなくメモリーを使用する必要があります。
結論:住所を取得したり、無頓着に渡したり保管したりすることは避けてください。
変数がメモリまたはレジスター(または場合によっては複数のレジスター)に格納されるかどうか(およびプロセッサーが決定するオプションがあると想定して、コンパイラーにどのオプションを与えるか)は、完全にコンパイラー(プロセッサー・タイプと組み合わせて)に依存します。そのようなこと-ほとんどの「良い」コンパイラは行います)。たとえば、LLVM/Clangコンパイラは、変数をメモリからレジスタに移動する「mem2reg」と呼ばれる特定の最適化パスを使用します。これを行うかどうかの決定は、変数の使用方法に基づいています。たとえば、ある時点で変数のアドレスを取得する場合、変数はメモリ内にある必要があります。
他のコンパイラは、機能は類似していますが、必ずしも同一ではありません。
また、少なくとも移植性の類似性があるコンパイラでは、実際のターゲットの生成マシンコードのフェーズも存在します。これには、ターゲット固有の最適化が含まれ、変数をメモリからレジスタに移動できます。
[特定のコンパイラがどのように機能するかを理解せずに]コード内の変数がレジスタ内にあるかメモリ内にあるかを判断することはできません。推測することはできますが、そのような推測は、他の「予測可能なものの種類」を推測するのと同じです。たとえば、窓から数時間で雨が降るかどうかを推測するのと同じです。 、またはかなり予測可能-一部の熱帯の国では、雨が午後に到着する時刻に基づいて時計を設定できます。他の国ではめったに雨が降らず、ここイギリスなど一部の国では、それ以上のことを知ることができません "今は雨が降っていない」.
実際の質問に答えるには:
g
への引数がスタックにあることは保証されていません。これは多くのオプションの1つにすぎません。多くのABI(アプリケーションバイナリインターフェイス、別名「呼び出し規約」)は、関数の最初の数個の引数にレジスタを使用します。そのため、コンパイラがターゲットとするコンパイラ(ある程度)とプロセッサ(コンパイラよりもはるかに多い)に依存します。引数がメモリ内にあるかレジスタ内にあるか。x
、y
のレジスタを "解放"した場合のコストによって異なります。 z
-これは、「まったく費用がかからない」から「少しだけ」までの範囲です-再び、プロセッサモデルとABIによって異なります。コンピュータがメインメモリにある一部のデータに対していくつかの操作を行うには、データをいくつかのレジスタに最初にロードしてから、CPUがそのデータを操作できるというのは本当ですか?
このステートメントでさえ常に正しいとは限りません。これはおそらく、これから作業するすべてのプラットフォームに当てはまりますが、 プロセッサレジスタ をまったく使用しない別のアーキテクチャが存在する可能性もあります。
ただし、x86_64コンピュータでは可能です。
f()は、g(x、y、z)が関数であるのと同じように関数です。g()はレジスターにあり、g()で渡されるパラメーターはスタックにありますか?
X、y、zの宣言により、それらがレジスタに格納されることがどのようにしてわかりますか? g()内のデータはどこに保存、登録、またはスタックされますか?
これらの2つの質問は、コードがコンパイルされるコンパイラおよびシステムに対して一意に回答することはできません。 g
のパラメーターがスタックにない可能性があるため、それらを当然と見なすこともできません。これはすべて、以下で説明するいくつかの概念に依存します。
まず、いわゆる 呼び出し規約 に注意する必要があります。これは、特に、関数パラメーターがどのように渡されるか(たとえば、スタックにプッシュされるか、レジスターに配置されるか、または両方の組み合わせ)を定義します。これはC++標準では強制されておらず、呼び出し規約は [〜#〜] abi [〜#〜] の一部であり、低レベルのマシンコードプログラムの問題に関する幅広いトピックです。
次に レジスタ割り当て (つまり、どの変数がいつでも実際にレジスタにロードされるか)は複雑なタスクであり、 NP-complete の問題です。コンパイラーは、入手した情報を最大限に活用しようとします。一般に、アクセス頻度の低い変数はスタックに入れられ、アクセス頻度の高い変数はレジスタに保持されます。したがって、部分Where the data inside g() is stored, register or stack?
は答えられませんonce-and-for-allregisterを含む多くの要因に依存するため圧力 。
コンパイラーの最適化は言うまでもありません。これにより、いくつかの変数を使用する必要がなくなります。
最後に、あなたがリンクした質問はすでに述べています
当然のことながら、マイレージはさまざまで、この特定のFAQの範囲外の変数は数十億ありますが、上記は、手続き型の統合で発生する可能性のある種類の例として機能します。
つまり、投稿した段落は、例を設定するためのいくつかの前提条件を備えています。これらは単なる仮定であり、そのように扱う必要があります。
小さな追加として:関数に対するinline
の利点に関して、私はこの答えを確認することをお勧めします: https://stackoverflow.com/a/145952/193816
変数がレジスタ、スタック、ヒープ、グローバルメモリなどにあるかどうかは、アセンブリ言語を調べないとわかりません。 変数は抽象的な概念です。コンパイラーは、レジスターまたは他のメモリーを選択どおりに使用できます実行が変更されない限り。
このトピックに影響を与える別のルールもあります。変数のアドレスを取得してポインタに格納すると、レジスタにアドレスがないため、変数がレジスタに配置されない場合があります。
変数のストレージは、コンパイラーの最適化設定にも依存する場合があります。変数は単純化のために消えることがあります。値を変更しない変数は、実行可能ファイルに定数として配置できます。
あなたの#1の質問に関して、はい、ロード/ストア以外の命令はレジスタで動作します。
#2の質問については、パラメーターがスタックで渡されると想定している場合、レジスタをスタックに書き込む必要があります。そうでない場合、g()は、 g()のコードは、パラメータが登録されていることを「認識」していないため、データです。
#3の質問に関しては、x、y、zがf()のレジスタに確実に格納されるかどうかはわかりません。 register
キーワードを使用することもできますが、これは推奨事項です。呼び出し規約に基づいて、コンパイラーがパラメーターの受け渡しを伴う最適化を行わないと想定すると、パラメーターがスタック上にあるかレジスター内にあるかを予測できる場合があります。
呼び出し規約に慣れる必要があります。呼び出し規約は、パラメーターが関数に渡される方法を扱い、通常、指定された順序でスタックにパラメーターを渡すこと、パラメーターをレジスターに入れること、または両方の組み合わせを行うことを含みます。
stdcall
、cdecl
、およびfastcall
は、呼び出し規約のいくつかの例です。パラメーターの受け渡しに関しては、stdcallとcdeclは同じです。パラメーターは右から左の順序でスタックにプッシュされます。この場合、g()
がcdecl
またはstdcall
の場合、呼び出し元はz、y、xをこの順序でプッシュします。
mov eax, z
Push eax
mov eax, x
Push eax
mov eax, y
Push eax
call g
64ビットfastcallでは、レジスターが使用され、MicrosoftはRCX、RDX、R8、R9(および4つ以上のパラメーターを必要とする関数のスタック)を使用し、linuxはRDI、RSI、RDX、RCX、R8、R9を使用します。 g()を呼び出すには、MS 64ビットfastcallを使用して次のようにします(z
、x
、およびy
がレジスタ)
mov rcx, x
mov rdx, y
mov r8, z
call g
このようにして、Assemblyは人間によって、場合によってはコンパイラによって記述されます。コンパイラーはいくつかのトリックを使用してパラメーターの受け渡しを回避します。これは通常、命令の数を減らし、メモリーへのアクセス回数を減らすことができるためです。例として次のコードを見てください(私は不揮発性レジスタルールを意図的に無視しています)。
f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
call g
mov rcx, rax
ret
g:
mov rax, rsi
add rax, rcx
add rax, rdx
ret
説明のために、rcxはすでに使用されており、xはrsiにロードされています。コンパイラは、rcxの代わりにrsiを使用するようにgをコンパイルできるため、gを呼び出すときに2つのレジスタ間で値を交換する必要はありません。コンパイラは、gをインライン化することもできます。これで、fとgは、x、y、zの同じレジスタセットを共有します。その場合、call g
命令は、ret
命令を除いて、gの内容に置き換えられます。
f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
mov rax, rsi
add rax, rcx
add rax, rdx
mov rcx, rax
ret
Gがfにインライン化されているため、call
命令を処理する必要がないため、これはさらに高速になります。
短い答え:できません。コンパイラーと有効になっている最適化機能に完全に依存します。
コンパイラの問題は、プログラムをアセンブリに変換することですが、その方法はコンパイラの動作と密接に関連しています。一部のコンパイラでは、登録する変数マップをヒントにすることができます。たとえば、これを確認してください: https://gcc.gnu.org/onlinedocs/gcc/Global-Reg-Vars.html
コンパイラは、何かを得るためにコードに変換を適用し、パフォーマンスを向上させ、コードサイズを小さくし、コスト関数を適用してこのゲインを推定するため、通常はコンパイルされたユニットを逆アセンブルした結果しか確認できません。
コンピュータがメインメモリにある一部のデータに対していくつかの操作を行うには、データをいくつかのレジスタに最初にロードしてから、CPUがそのデータを操作できるというのは本当ですか?
これは、アーキテクチャとそれが提供する命令セットによって異なります。しかし、実際には、はい-それは典型的なケースです。
X、y、zの宣言により、それらがレジスタに格納されることがどのようにしてわかりますか? g()内のデータはどこに保存、登録、またはスタックされますか?
コンパイラーがローカル変数を除去しないと仮定すると、レジスターはスタック(メイン・メモリーまたはキャッシュに常駐する)よりも高速であるため、それらをレジスターに入れる方が好まれます。
しかし、これは普遍的な真実にはほど遠いです。それは、コンパイラーの(複雑な)内部動作に依存します(詳細はその段落で手動で振られています)。
f()は、g(x、y、z)が関数であるのと同じように関数です。g()はレジスターにあり、g()で渡されるパラメーターはスタックにありますか?
変数が実際にはレジスターに格納されていると仮定した場合でも、関数を呼び出すと、 calling Convention =起動します。これは、関数がどのように呼び出され、引数が渡され、スタックをクリーンアップするか、どのレジスターが保持されるかを説明する規則です。
すべての呼び出し規約には、何らかのオーバーヘッドがあります。このオーバーヘッドの原因の1つは、引数の受け渡しです。多くの呼び出し規約は、レジスタを介して引数を渡すことを優先することでそれを削減しようとしますが、CPUレジスタの数は(スタックのスペースと比較して)制限されているため、最終的には多数の引数の後にスタックをプッシュすることにフォールバックします。
あなたの質問の段落は、すべてをスタックに渡す呼び出し規則を想定しており、その仮定に基づいて、それを伝えようとしているのは、(コンパイル時に)「コピー」できれば、(実行速度に関して)有益であるということです。 (関数の呼び出しを発行するのではなく)呼び出し元内の呼び出された関数の本体。これにより、論理的には同じ結果が得られますが、関数呼び出しの実行時のコストがなくなります。
変数はほとんどの場合、メインメモリに格納されます。多くの場合、コンパイラーの最適化により、宣言された変数の値はメインメモリに移動しませんが、それらはメソッドで使用する中間変数であり、他のメソッドが呼び出される前に関連性を保持しません(つまり、スタック操作の発生)。
これは設計によるものです。プロセッサーがレジスター内のデータをアドレス指定して操作するのがより簡単(かつ高速)になるため、パフォーマンスが向上します。建築用レジスターはサイズに制限があるため、すべてをレジスターに入れることはできません。コンパイラーにレジスターに入れるように「ヒント」を与えたとしても、使用可能なレジスターがいっぱいになると、最終的にOSはそれをレジスターの外部、メイン・メモリーで管理する可能性があります。
おそらく、変数はnearの実行でさらに関連性を保持し、より長い期間のCPU時間の依存性を保持する可能性があるため、メインメモリに存在します。変数は、今後の機械語命令との関連性を保持し、実行はほぼimmediateになるため、アーキテクチャレジスタにありますが、長い間は関係がない可能性があります。