1. ホーム
  2. c#

[解決済み】スタックの目的は何ですか?なぜそれが必要なのですか?

2022-03-26 11:32:21

質問

そこで、C# .NETアプリケーションのデバッグを学ぶために、今、MSILを勉強しています。

ずっと不思議に思っていたんです。 スタックは何のためにあるのですか?

私の質問を整理しておくと

なぜメモリからスタックへの転送や"loading?"があるのでしょうか。 一方、スタックからメモリへの転送、つまり "storing"はなぜあるのでしょうか? なぜ、すべてメモリに配置させないのか?

  • 速くなったからでしょうか?
  • RAMベースだからでしょうか?
  • 効率化のため?

これを把握することで、理解を深めたい CIL のコードをより深く理解することができます。

解決方法は?

UPDATE: この質問がとても気に入ったので、作りました。 2011年11月18日、私のブログのテーマとなりました。 . 素晴らしい質問をありがとうございました。

<ブロッククオート

いつも不思議に思うのですが、スタックの目的は何ですか?

のことを指しているのでしょう。 評価スタック のMSIL言語のスタックであって、実行時の実際のスレッド毎のスタックではありません。

<ブロッククオート

逆に、なぜメモリからスタックへの転送、つまり読み込みが必要なのでしょうか?なぜ、すべてメモリに配置しないのでしょうか?

MSILは、"仮想マシン"言語です。C#コンパイラのようなコンパイラが生成するのは CIL そして、実行時にJIT(Just In Time)コンパイラと呼ばれる別のコンパイラが、ILを実行可能な実際のマシンコードに変換する。

C#コンパイラが機械語を書き出すだけではだめなのでしょうか?

なぜなら、それは 安い このようにすることです。仮にこの方法でなく、各言語が独自のマシンコードジェネレータを持たなければならないとします。20種類の言語があるとします。C#, JScript .NET Visual Basicです。 IronPython , F# ... そして、10種類のプロセッサーがあるとします。何個のコードジェネレータを書かなければならないか?20 x 10 = 200個のコード・ジェネレータが必要です。これは大変な作業です。では、新しいプロセッサを追加するとしよう。そのためのコード・ジェネレータを、各言語ごとに20回書かなければならない。

しかも、難しくて危険な作業です。専門家でもないチップのために効率的なコードジェネレータを書くのは、大変な仕事なのです コンパイラの設計者は、言語の意味解析の専門家であって、新しいチップセットの効率的なレジスタ割り当ての専門家ではないのです。

では、CIL方式でやるとします。CILジェネレータを何個書けばいいのでしょうか?1言語につき1つです。JITコンパイラは何本書けばいいのでしょうか?プロセッサごとに1つ。合計:20+10=30個のコード・ジェネレータ。さらに、CILは単純な言語なので、言語からCILへの生成器は簡単に書けますし、CILからマシン・コードへの生成器も単純な言語なので、簡単に書けます。C#やVBなどの複雑なものをすべて取り除き、すべてをジッターを書きやすいシンプルな言語へと"lower"しています。

中間言語があることで、新しい言語のコンパイラーを作るコストを下げることができる 劇的に . また、新しいチップをサポートするコストも劇的に下がります。新しいチップをサポートしたい場合、そのチップの専門家を探して、CILジッターを書かせれば完了です。

では、なぜMSILがあるのか、それは中間言語があるとコストが下がるからです。では、なぜその言語は「スタックマシン」なのでしょうか?

なぜなら、スタックマシンは言語コンパイラを書く人にとって、概念的に非常に扱いやすいからです。スタックは、計算を記述するためのシンプルで理解しやすいメカニズムです。また、スタックマシンは、JITコンパイラの開発者にとっても概念的に非常に扱いやすいものです。スタックを使うことは、抽象化を単純化することであり、したがってまた <項目 コストを下げることができる .

なぜスタックが必要なのか、すべてメモリから直接実行すればいいのでは?では、考えてみましょう。例えば、以下のようなCILコードを生成したいとします。

int x = A() + B() + C() + 10;

例えば、add", call", store" などは、常に引数をスタックから取り出し、その結果をスタックに格納する、という規約があるとします。このC#のCILコードを生成するには、次のように言うだけでよい。

load the address of x // The stack now contains address of x
call A()              // The stack contains address of x and result of A()
call B()              // Address of x, result of A(), result of B()
add                   // Address of x, result of A() + B()
call C()              // Address of x, result of A() + B(), result of C()
add                   // Address of x, result of A() + B() + C()
load 10               // Address of x, result of A() + B() + C(), 10
add                   // Address of x, result of A() + B() + C() + 10
store in address      // The result is now stored in x, and the stack is empty.

ここで、スタックなしでやったとします。あなたのやり方で、ここで 各オペコードはオペランドのアドレスとその結果を格納するアドレスを取る。 :

Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...

どうなるかわかりますか?このコードでは 巨大 なぜなら、すべての一時的なストレージを明示的に割り当てる必要があるからです。 通常であればスタックに格納するところを . さらに、結果を書き込むアドレスと各オペランドのアドレスを引数に取らなければならないので、オペコード自体が巨大化しています。スタックから2つのものを取り出して1つのものを乗せることがわかっているquot;add"命令は、1バイトになることがあります。2つのオペランドのアドレスと結果のアドレスを受け取る加算命令は、巨大になります。

スタックベースのオペコードを使用するのは スタックは共通の問題を解決します . すなわち 一時的なストレージを確保し、すぐに使って、終わったらすぐに捨てたい。 . スタックを自由に使えると仮定することで、オペコードを非常に小さくし、コードを非常に簡潔なものにすることができます。

UPDATE: いくつかの追加的な考え

ちなみに、(1)仮想マシンを指定し、(2)仮想マシンの言語をターゲットにしたコンパイラを書き、(3)さまざまなハードウェアに仮想マシンの実装を書くことで、コストを劇的に下げるという考え方は、まったく新しいものではないのです。MSIL、LLVM、Javaバイトコード、その他の最新のインフラストラクチャに由来するものではありません。私が知る限り、この戦略の最も初期の実装は pcodeマシン 1966年

個人的にこのコンセプトを初めて知ったのは、インフォコムの実装者がいかにして ゾーク 多くの異なるマシンでうまく動作する。という仮想マシンを指定したのです。 Zマシン そして、ゲームを走らせたいすべてのハードウェアのZ-machineエミュレータを作りました。これには、次のような大きなメリットがありました。 仮想メモリ管理 必要なときにディスクからコードをページングして、新しいコードをロードするときにはそれを破棄すればよいのですから、ゲームはメモリに収まるサイズではありません。