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


320

したがって、私は今、C#.NETアプリケーションのデバッグを学ぶためにMSILを学習しています。

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

ちょうど私の質問を文脈で述べるために:
なぜメモリからスタックまたは「ロード」への転送があるのですか?一方、スタックからメモリへの転送または「格納」があるのはなぜですか? それらすべてをメモリに配置しないのはなぜですか?

  • それはより速いからですか?
  • RAMベースだからですか?
  • 効率のため?

これを把握して、CILコードをより深く理解できるようにしています。


28
ヒープがメモリの別の部分であるように、スタックはメモリの一部です。
CodesInChaos

@CodeInChaosは、値型と参照型について話しているのですか?それともILコードに関して同じですか?...私はスタックがヒープよりも高速で効率的であることを知っています(ただし、値/参照型の世界ではこれが同じかどうかはわかりません)
Jan Carlo Viray 2011年

15
@CodeInChaos-Janが参照するスタックは、関数呼び出し中にスタックフレームを受け入れるメモリ領域とは対照的に、ILが書き込まれるスタックマシンだと思います。それらは2つの異なるスタックであり、JITの後、ILスタックは存在しません(とにかくx86上)
Damien_The_Unbeliever

4
MSILの知識は、.NETアプリケーションのデバッグにどのように役立ちますか?
Piotr Perak

1
最近のマシンでは、コードのキャッシング動作はパフォーマンスを左右するものです。メモリはどこにでもあります。スタックは通常、ここにあります。スタックが実際のものであり、一部のコードの操作を表現するために使用される概念だけではないと仮定します。MSILを実行するプラットフォームの実装では、スタックの概念がハードウェアで実際にビットをプッシュするようにする必要はありません。
モニカを

回答:


441

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

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

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

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

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

では、最初に「なぜMSILがまったくないのか」という質問に答えましょう。なぜC#コンパイラにマシンコードを書き出さないのですか?

この方法で行うほう安価だからです。そのようにしていないとしましょう。各言語に独自のマシンコードジェネレーターが必要であるとします。20の異なる言語があります: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の複雑な要素をすべて取り除き、すべてをジッターを記述しやすい単純な言語に「低く」します。

中間言語を使用すると、新しい言語コンパイラを作成するコストが大幅に削減されます。また、新しいチップをサポートするコストを大幅に削減します。新しいチップをサポートしたい場合、そのチップの専門家を見つけて、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ビットシステムに仮想メモリ管理を実装できるという、さらに大きなメリットがありました。必要なときにディスクからコードをページインし、新しいコードをロードする必要があるときにゲームを破棄できるため、ゲームはメモリに収まるよりも大きくなる可能性があります。


63
ワオ。それはまさに私が探していたものです。回答を得るための最良の方法は、主要な開発者自身から回答を取得することです。お時間をいただきありがとうございます。これは、コンパイラとMSILの複雑さを気にかけるすべての人に役立つと確信しています。エリック、ありがとう。
ジャンカルロViray

18
それは素晴らしい答えでした。私がJavaの人なのに、なぜあなたのブログを読んだのか思い出させてくれます。;-)
jprete 2010年

34
@JanCarloViray:どういたしまして!私プリンシパル開発者でありプリンシパル開発者ではないことに注意してください。このチームにはその役職の人が何人かいて、私は彼らの中で最年長でもありません。
Eric Lippert、2011年

17
@エリック:もしあなたがコーディングを愛するのをやめたら、プログラマーを教えることを検討すべきです。楽しみのほかに、あなたはビジネスのプレッシャーなしで殺害をしているかもしれません。素晴らしい才能はあなたがその分野で得たものです(そして素晴らしい忍耐力、私は付け加えるかもしれません)。私は元大学の講師としてそれを言います。
2011年

19
私の中で約4段落で「これはエリックのように聞こえます」と言っていました。
Binary Worrier '25年

86

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

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

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

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


2
+1で非常に詳細な説明を、+ 100(可能な場合)で他のシステムや言語との詳細な比較を追加:)
Jan Carlo Viray

4
DalvikがRegisterマシンなのはなぜですか?Sicneは主にARMプロセッサをターゲットとしています。現在、x86には同じ数のレジスターがありますが、CISCであり、残りの4つは暗黙的に一般的な命令で使用されるため、ローカルの格納に実際に使用できるのはそのうちの4つだけです。一方、ARMアーキテクチャには、ローカルを格納するために使用できるレジスタがはるかに多いため、レジスタベースの実行モデルが容易になります。
ヨハネスルドルフ

1
@JohannesRudolphそれは今ではほぼ20年間真実ではありません。ほとんどのC ++コンパイラが依然として90年代のx86命令セットをターゲットにしているからといって、x86自体が効率的であるとは限りません。Haswellには、たとえば、168個の汎用整数レジスタと168個のGP AVXレジスタがあります。(モダン)x86アセンブリのすべてを、好きな方法で使用できます。アーキテクチャ/ CPUではなくコンパイラライターのせいにします。実際、これが中間コンパイルが魅力的である理由の1つです。1つのバイナリで、特定のCPUに最適なコードです。90年代の建築物にこだわる必要はありません。
Luaan

2
@JohannesRudolph .NET JITコンパイラは実際にはレジスタをかなり頻繁に使用します。スタックは主にIL仮想マシンの抽象概念であり、実際にCPUで実行されるコードは大きく異なります。メソッド呼び出しはパスバイレジスタ、ローカルはレジスタにリフトされるかもしれません...マシンコードのスタックの主な利点は、それがサブルーチン呼び出しに与える分離です-ローカルをレジスタに置くと、関数呼び出しはあなたはその価値を失い、あなたは本当に言うことができません。
Luaan 16

1
@RahulAgarwal生成されたマシンコードは、特定のローカル値または中間値のスタックを使用する場合と使用しない場合があります。ILでは、すべての引数とローカルはスタック上にありますが、マシンコードではこれは当てはまりません(許可されてますが、必須ではありません)。いくつかのものがスタックで役立ち、それらはスタックに置かれます。ヒープ上で役立つものもあり、それらはヒープ上に置かれます。まったく必要のないものや、レジスターにほんの少しの時間が必要なものもあります。呼び出しを完全に削除するか(インライン化)、または引数をレジスターで渡すことができます。JITには多くの自由があります。
Luaan

20

これに関する非常に興味深い/詳細なWikipediaの記事「スタックマシン命令セットの利点」があります。完全に引用する必要があるので、リンクを張るほうが簡単です。字幕を引用します

  • 非常にコンパクトなオブジェクトコード
  • 単純なコンパイラー/単純なインタープリター
  • 最小限のプロセッサ状態

-1 @xanatos取った見出しをまとめてみてください。
Tim Lloyd

@chibacityそれらを要約したいなら、私は答えを出したでしょう。私は非常に良いリンクを引き出そうとしていました。
xanatos

@xanatos私はあなたの目標を理解していますが、そのような大きなWikipediaの記事へのリンクを共有することは良い答えではありません。グーグルするだけで見つけるのは難しくありません。一方、ハンスは素晴らしい答えを持っています。
Tim Lloyd

@chibacity OPは最初に検索しなかったので、おそらく怠惰でした。ここの回答者は良いリンクを提供しました(説明はありません)。2つの悪が1つの良いことをします:-)そして、私はハンスに賛成します。
xanatos

素晴らしいリンクのために回答者と@xanatos +1に。私は誰かが完全に要約してナレッジパックの答えを得るのを待っていました..ハンスが答えを出さなかったら、私はあなたの答えを受け入れられた答えにしたでしょう。それは単なるリンクだっただけなのでそうではありませんでした回答に力を入れたハンスのためのフェア.. :)
Jan Carlo Viray

8

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


5

スタック/ヒープの概念が守られておらず、データがランダムなメモリの場所にロードされている場合、またはデータがランダムなメモリの場所から格納されている場合...非常に構造化されておらず、管理されていません。

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


4

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

Andrew Appelの以前の執筆を参照してください。継続ガベージコレクションを使用したコンパイルは、スタック割り当てよりも高速です。

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


0

私は「割り込み」を探しましたが、誰もそれを利点として含めませんでした。マイクロコントローラまたはその他のプロセッサに割り込むデバイスごとに、通常、スタックにプッシュされるレジスタがあり、割り込みサービスルーチンが呼び出され、それが完了すると、レジスタはスタックからポップバックされ、そこに戻されますだった。次に、命令ポインタが復元され、通常のアクティビティが中断したところから再開します。まるで割り込みが発生しなかったかのようです。スタックを使用すると、実際には複数のデバイスに(理論的には)互いに割り込みをかけることができ、スタックがあるため、すべてが正常に機能します。

連結型言語と呼ばれるスタックベースの言語のファミリーもあります。スタックは暗黙的に渡されるパラメーターであり、変更されたスタックは各関数からの暗黙的な戻りであるため、これらはすべて(私は)関数型言語です。両方フォース因子(優れている)他の人と一緒に、一例です。FactorはスクリプトゲームでLuaと同様に使用され、現在Appleで働いている天才のSlava Pestovによって書かれました。私が何度か見たyoutubeの彼のGoogle TechTalk。彼はボアのコンストラクタについて語っていますが、彼が何を意味するのかわかりません;-)。

JVM、MicrosoftのCILなどの現在のVMの一部、さらに私がLua用に作成したものでさえ、これらのスタックベースの言語のいくつかで作成して、さらに多くのプラットフォームに移植できるようにする必要があると私は本当に思います。これらの連結言語には、VM作成キットとしての呼び出しや、移植性プラットフォームがどういうわけか欠落していると思います。ANSI Cで記述された「移植性のある」ForthであるpForthさえあり、さらに汎用性の高い移植性のために使用できます。誰かがEmscriptenまたはWebAssemblyを使用してコンパイルしようとしましたか?

スタックベースの言語では、ゼロポイントと呼ばれるコードのスタイルがあります。パラメーターをまったく渡さずに(時々)呼び出される関数をリストするだけだからです。関数が完全に適合している場合、すべてのゼロポイント関数のリストしかなく、それが(理論的には)アプリケーションになります。ForthかFactorのどちらかを詳しく調べれば、私が話していることがわかります。

フォース簡単に、JavaScriptで書かれた素敵なオンラインチュートリアル、ここに小さなサンプルが(ゼロ点呼び出しスタイルの一例として、「SQ SQ SQ SQ」に注意してください)です。

: sq dup * ;  ok
2 sq . 4  ok
: ^4 sq sq ;  ok
2 ^4 . 16  ok
: ^8 sq sq sq sq ;  ok
2 ^8 . 65536  ok

また、Easy ForthのWebページのソースを見ると、下部に、それが非常にモジュール化されており、約8つのJavaScriptファイルで記述されていることがわかります。

私はForthを同化するために手に入れることができるほぼすべてのForthの本に多くのお金を費やしましたが、今はそれをよりよく理解し始めています。私が後から来る人たちに頭を上げたいと思います。もしあなたが本当にそれを手に入れたいのであれば(私はこれを手遅れに見つけました)、FigForthの本を入手して実装します。商用のForthは複雑すぎるため、Forthの最大の特長は、システム全体を上から下まで理解できることです。どういうわけか、フォース新しいプロセッサの全体の開発環境を実装し、しかし必要性すべてがCで合格しているように見えるため、Forthを最初から作成することは、通過儀礼として依然として役立ちます。したがって、これを選択する場合は、FigForthブックを試してください。これは、さまざまなプロセッサに同時に実装された複数のForthです。フォースのロゼッタストーンの一種。

スタックが必要な理由-効率、最適化、ゼロポイント、割り込み時のレジスタの保存、そして再帰的なアルゴリズムの場合、それは「正しい形」です。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.