web-dev-qa-db-ja.com

スタックの目的は何ですか?なぜ必要なのですか?

だから私は今、C#.NETアプリケーションのデバッグを学ぶためにMSILを学んでいます。

私はいつも疑問に思っていました:スタックの目的は何ですか?

私の質問をコンテキストに入れてください:
メモリからスタックまたは「ロード」への転送があるのはなぜですか?一方、スタックからメモリへの転送または「保存」があるのはなぜですか? すべてをメモリに配置するだけではどうですか?

  • 速いからでしょうか?
  • それはRAMベースですか?
  • 効率のために?

[〜#〜] cil [〜#〜] コードをより深く理解するためにこれを把握しようとしています。

315
Jan Carlo Viray

更新:私はこの質問をとても気に入りました 2011年11月18日の私のブログの主題 。すばらしい質問をありがとう!

私はいつも疑問に思っていました:スタックの目的は何ですか?

実行時の実際のスレッドごとのスタックではなく、MSIL言語の評価スタックを意味すると仮定します。

メモリからスタックまたは「ロード」への転送があるのはなぜですか?一方、スタックからメモリへの転送または「保存」があるのはなぜですか?なぜそれらをすべてメモリに配置しないのですか?

MSILは「仮想マシン」言語です。 C#コンパイラなどのコンパイラは [〜#〜] cil [〜#〜] を生成し、実行時にJIT(Just In Time)コンパイラと呼ばれる別のコンパイラがILを実行可能な実際のマシンコードに変換します。 。

それでは、まず「なぜMSILを使用しているのですか?」という質問に答えましょう。なぜC#コンパイラにマシンコードを書き出させないのですか?

このようにするのは安いです。そのようにしなかったとします;各言語に独自のマシンコードジェネレーターが必要であるとします。C#、 JScript。 NET 、Visual Basic、 IronPythonF# ...そして、10個の異なるプロセッサがあるとします。コードジェネレータをいくつ作成する必要がありますか?20 x 10 = 200個のコードジェネレーターこれは大変な作業です。新しいプロセッサを追加する場合、各言語に1つずつ、20回コードジェネレーターを作成する必要があります。

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

ここで、CILの方法で実行するとします。いくつのCILジェネレーターを作成する必要がありますか?言語ごとに1つ。いくつのJITコンパイラーを作成する必要がありますか?プロセッサごとに1つ。合計:20 + 10 = 30コードジェネレーター。さらに、CILは単純な言語であるため、言語からCILへのジェネレーターは記述しやすく、CILは単純な言語であるため、CILからマシンコードへのジェネレーターも記述しやすいです。 C#とVBとwhatnotのすべての複雑さを取り除き、すべてを「下げる」ことで、ジッターを簡単に記述できる単純な言語にします。

中間言語を使用すると、新しい言語コンパイラを作成するコストが大幅に削減されます劇的に。また、新しいチップをサポートするコストが大幅に削減されます。新しいチップをサポートしたい場合は、 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
...

これがどうなるかわかりますか?私たちのコードはhugeを取得しています。これは、すべての一時記憶域を明示的に割り当てる必要があるためです慣例により、通常はスタックに移動します。さらに悪いことに、オペコード自体がすべて巨大になっています引数として、結果を書き込むアドレスと各オペランドのアドレススタックから2つのものを取り出して1つのものを置くことを知っている「add」命令は、単一のバイト:2つのオペランドアドレスと結果アドレスを使用する加算命令は膨大になります。

スタックは一般的な問題を解決するため、スタックベースのオペコードを使用します。つまり:一時ストレージを割り当てて、すぐに使用し、すぐにそれを取り除くm done。使用可能なスタックがあると仮定することにより、オペコードを非常に小さく、コードを非常に簡潔にすることができます。

更新:追加の考え

ちなみに、(1)仮想マシンの指定、(2)VM言語をターゲットとするコンパイラの作成、および(3)VMは、まったく新しいアイデアではありません。MSIL、LLVM、Javaバイトコード、またはその他の最新のインフラストラクチャに由来するものではありません。私が知っているこの戦略は、1966年からの pcode machine です。

私が個人的にこの概念を聞いたのは、Infocomの実装者が、非常に多くの異なるマシンで Zork を実行する方法を学んだときでした。 Z-machine と呼ばれる仮想マシンを指定し、ゲームを実行したいすべてのハードウェア用のZ-machineエミュレーターを作成しました。これには、プリミティブな8ビットシステムに仮想メモリ管理を実装できるという大きな利点がありました。ゲームは、必要なときにディスクからコードをページングして破棄できるため、メモリに収まるよりも大きくなる可能性があります新しいコードを読み込む必要があるときに.

434
Eric Lippert

MSILについて話しているときは、virtualマシンの指示について話していることに注意してください。 .NETで使用されるVMは、スタックベースの仮想マシンです。レジスタベースのVMとは対照的に、Androidオペレーティングシステムで使用される Dalvik VM はその一例です。

VMのスタックは仮想です。VM命令をプロセッサで実行される実際のコードに変換するのは、インタープリターまたはジャストインタイムコンパイラー次第です。 .NETの場合、ほとんど常にジッターであるため、MSIL命令セットは最初から手間がかからないように設計されています。たとえばJavaバイトコードとは対照的に、特定のデータ型に対する操作の明確な指示があります。これにより、解釈が最適化されます。 MSILインタープリターは実際に存在しますが、.NET Micro Frameworkで使用されます。リソースが非常に限られているプロセッサで実行されるため、マシンコードの保存に必要なRAMを購入する余裕がありません。

実際のマシンコードモデルは混合されており、スタックとレジスタの両方があります。 JITコードオプティマイザーの大きな仕事の1つは、スタックに保持されている変数をレジスタに保存する方法を考え出すことです。これにより、実行速度が大幅に向上します。 Dalvikジッタには逆の問題があります。

それ以外の場合、マシンスタックは非常に基本的なストレージ機能であり、非常に長い間プロセッサの設計に使用されてきました。参照の局所性が非常に良好です。これは、RAMがデータを提供し、再帰をサポートするよりもはるかに高速にデータをかみ砕く最新のCPUの非常に重要な機能です。言語設計は、スタックを持つことによって大きく影響され、ローカル変数のサポートとメソッド本体に限定されたスコープでサポートされます。スタックの重大な問題は、このサイトの名前です。

86
Hans Passant

これに関する非常に興味深い/詳細なウィキペディアの記事があります、スタックマシン命令セットの利点。完全に引用する必要があるので、単純にリンクを置く方が簡単です。私は単にサブタイトルを引用します

  • 非常にコンパクトなオブジェクトコード
  • シンプルなコンパイラー/シンプルなインタープリター
  • 最小プロセッサ状態
20
user468687

スタックの質問にもう少し追加します。スタックの概念は、算術論理ユニット(ALU)のマシンコードがスタック上にあるオペランドで動作するCPU設計から派生しています。たとえば、乗算演算では、スタックから2つの上位オペランドを取得し、それらを乗算して、結果をスタックに戻すことができます。マシン言語には通常、スタックからオペランドを追加および削除するための2つの基本関数があります。プッシュとPOP。多くのcpuのdsp(デジタルシグナルプロセッサ)およびマシンコントローラー(洗濯機を制御するものなど)では、スタックはチップ自体に配置されています。これにより、ALUへのアクセスが高速になり、必要な機能が単一のチップに統合されます。

8
skyman

スタック/ヒープの概念に従わず、データがランダムなメモリ位置にロードされる場合ORデータはランダムなメモリ位置から保存されます...非常に構造化されておらず、管理されません。

これらの概念は、事前定義された構造にデータを格納して、パフォーマンス、メモリ使用量を改善するために使用されます。したがって、データ構造と呼ばれます。

5
Azodious

継続渡しスタイル のコーディングを使用することで、スタックなしでシステムを動作させることができます。次に、呼び出しフレームは、ガベージコレクションヒープに割り当てられた継続となります(ガベージコレクタにはスタックが必要になります)。

Andrew Appelの古い著作を参照してください: Continuations および でコンパイルするガベージコレクションは、Stack Allocation

(キャッシュの問題により、今日は少し間違っているかもしれません)