更新:2011年11月18日にこの質問が気に入り、私のブログの主題になりました。すばらしい質問をありがとう!
私はいつも疑問に思っていました:スタックの目的は何ですか?
実行時の実際のスレッドごとのスタックではなく、MSIL言語の評価スタックを意味していると思います。
メモリからスタックまたは「ロード」への転送があるのはなぜですか?一方、スタックからメモリへの転送または「格納」があるのはなぜですか?それらすべてをメモリに配置しないのはなぜですか?
MSILは「仮想マシン」言語です。C#コンパイラーのようなコンパイラーはCILを生成し、実行時にJIT(Just In Time)コンパイラーと呼ばれる別のコンパイラーがILを実行可能な実際のマシンコードに変換します。
では、最初に「なぜMSILがまったくないのか」という質問に答えましょう。なぜC#コンパイラにマシンコードを書き出さないのですか?
この方法で行うほうが安価だからです。そのようにしていないとしましょう。各言語に独自のマシンコードジェネレーターが必要であるとします。20の異なる言語があります:C#、JScript .NET、Visual Basic、IronPython、F# ...そして、10の異なるプロセッサがあるとします。いくつのコードジェネレータを書かなければなりませんか?20 x 10 = 200コードジェネレーター。それは大変な作業です。次に、新しいプロセッサを追加するとします。そのためのコードジェネレーターを各言語に1つずつ、20回記述する必要があります。
さらに、それは困難で危険な作業です。あなたが専門家ではないチップのための効率的なコードジェネレータを書くことは大変な仕事です!コンパイラの設計者は、新しいチップセットの効率的なレジスタ割り当てではなく、言語のセマンティック分析の専門家です。
ここで、CILの方法でそれを実行するとします。いくつのCILジェネレータを書く必要がありますか?言語ごとに1つ。いくつのJITコンパイラーを作成する必要がありますか?プロセッサーごとに1つ。合計:20 + 10 = 30コードジェネレーター。さらに、CILは単純な言語であるため、言語からCILへのジェネレーターは簡単に記述でき、CILは単純な言語であるため、CILからマシンコードへのジェネレーターも簡単に記述できます。私たちは、C#とVBの複雑な要素をすべて取り除き、すべてをジッターを記述しやすい単純な言語に「低く」します。
中間言語を使用すると、新しい言語コンパイラを作成するコストが大幅に削減されます。また、新しいチップをサポートするコストを大幅に削減します。新しいチップをサポートしたい場合、そのチップの専門家を見つけて、CILジッタを書き込んでもらい、完了です。次に、これらの言語をすべてチップでサポートします。
OK、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つ置くことを認識している「追加」命令は、1バイトにすることができます。2つのオペランドアドレスと結果アドレスを使用するadd命令は、非常に大きくなります。
スタックは一般的な問題を解決するため、スタックベースのオペコードを使用します。つまり、一時的なストレージを割り当てて、すぐに使用し、完了したらすぐに削除したいと考えています。スタックは自由に使えると仮定することで、オペコードを非常に小さく、コードを非常に簡潔にすることができます。
更新:いくつかの追加の考え
ちなみに、(1)仮想マシンを指定する、(2)VM言語をターゲットとするコンパイラを作成する、(3)さまざまなハードウェアにVMの実装を作成することで、コストを大幅に削減するというこのアイデアは、まったく新しいアイデアではありません。 。これは、MSIL、LLVM、Javaバイトコード、またはその他の最新のインフラストラクチャでは発生しませんでした。私が知っているこの戦略の最も初期の実装は、1966年のpcodeマシンです。
私がこの概念について最初に私が聞いたのは、Infocomの実装者がZorkを非常に多くの異なるマシンでうまく実行する方法を知ったときでした。彼らはZマシンと呼ばれる仮想マシンを指定し、ゲームを実行したいすべてのハードウェア用にZマシンエミュレーターを作成しました。これには、プリミティブな8ビットシステムに仮想メモリ管理を実装できるという、さらに大きなメリットがありました。必要なときにディスクからコードをページインし、新しいコードをロードする必要があるときにゲームを破棄できるため、ゲームはメモリに収まるよりも大きくなる可能性があります。