web-dev-qa-db-ja.com

関数呼び出しのセマンティクスを表すためにスタックを使用する代わりの方法はどれですか?

関数呼び出しは通常スタックを使用して実装されることを私たちは皆知っています。フレーム、戻りアドレス、パラメータ、全体があります。

ただし、スタックは実装の詳細です。呼び出し規約はさまざまなことを行い(x86 fastcallは(一部の)レジスタを使用し、MIPSとフォロワーはレジスタウィンドウを使用するなど)、最適化は他のこと(インライン化、フレームポインタの省略、テールコールの最適化。

もちろん、多くのマシン(JVMやCLRのようなVMだけでなく、Push/POPを備えたx86などの実際のマシン)にも便利なスタック命令が存在するため、関数呼び出しに使用すると便利ですが、場合によっては可能ですコールスタックが不要な方法でプログラミングする(ここでは継続渡しスタイル、またはメッセージパッシングシステムのアクターについて考えています)

だから、私は不思議に思っていました:スタックなしで、またはそれ以上に、異なるデータ構造(おそらく、キュー、または連想マップ)を使用して関数呼び出しセマンティクスを実装することは可能ですか?
もちろん、私はスタックが非常に便利であることを理解しています(ユビキタスである理由があります)が、最近、不思議に思う実装にぶつかりました。

言語/マシン/仮想マシンでこれまでに行われたことがあるかどうかを知っている人はいますか?そうである場合、印象的な違いと欠点はどれですか?

編集:私の直感は、異なるサブ計算アプローチが異なるデータ構造を使用できるということです。たとえば、ラムダ計算はスタックベースではありません(関数適用のアイデアは簡約化によって捉えられています)が、実際の言語/マシン/例を調べていました。それが私が尋ねている理由です...

20

言語によっては、呼び出しスタックを使用する必要がない場合があります。呼び出しスタックは、再帰または相互再帰を許可する言語でのみ必要です。言語が再帰を許可しない場合、任意の時点で任意のプロシージャの1つの呼び出しのみがアクティブになり、そのプロシージャのローカル変数が静的に割り当てられる可能性があります。このような言語は、割り込み処理のためにコンテキストの変更に備える必要がありますが、これにはまだスタックは必要ありません。

呼び出しスタックを必要としない言語の例については、FORTRAN IV(およびそれ以前)および初期のバージョンのCOBOLを参照してください。

コールスタックに直接ハードウェアサポートを提供しなかった、非常に成功した初期のスーパーコンピューターの例については、Control Data 6600(および以前のControl Dataマシン)を参照してください。コールスタックをサポートしなかった非常に成功した初期のミニコンピューターの例については、PDP-8を参照してください。

私の知る限り、バローズB5000スタックマシンは、ハードウェアコールスタックを備えた最初のマシンでした。 B5000マシンは、再帰が必要なALGOLを実行するようにゼロから設計されました。また、機能アーキテクチャの基礎となった、最初の記述子ベースのアーキテクチャの1つもありました。

私の知る限り、MIT)のハッカーコミュニティが1つの配信を受け取り、 PUSHJ(Push Return Address and Jump)操作により、10進数の印刷ルーチンを50命令から10命令に減らすことができました。

再帰を可能にする言語での最も基本的な関数呼び出しのセマンティクスには、スタックとうまく一致する機能が必要です。それで十分な場合は、基本的なスタックが良いシンプルなマッチです。それ以上必要な場合は、データ構造でさらに多くのことを行う必要があります。

私が遭遇したものをもっと必要とする最良の例は、「継続」、つまり、途中で計算を一時停止し、それを凍結状態のバブルとして保存し、後で、おそらく何度も、再び発射する機能です。継続は、特にエラー出口を実装する方法として、LISPのScheme方言で一般的になりました。継続には、現在の実行環境のスナップショットを作成し、後でそれを再現する機能が必要です。そのため、スタックは多少不便です。

Abelson&Sussmanの「コンピュータプログラムの構造と解釈」 は、継続の詳細について説明しています。

20
John R. Strohm

なんらかのスタックを使用せずに関数呼び出しセマンティクスを実装することはできません。 Wordゲームでしかプレイできません(たとえば、「FILOリターンバッファー」などの別の名前を使用します)。

関数呼び出しのセマンティクスを実装しないもの(たとえば、継続渡しスタイル、アクター)を使用して、その上に関数呼び出しのセマンティクスを構築することができます。ただし、これは、関数が戻るときに制御が渡される場所を追跡するために、ある種のデータ構造を追加することを意味します。そのデータ構造は、一種のスタック(または名前/説明が異なるスタック)になります。

相互に呼び出すことができる多くの関数があるとします。実行時に、各関数は、関数が終了したときに戻る場所を知っている必要があります。 firstsecondを呼び出すと、次のようになります。

second returns to somewhere in first

次に、secondthirdを呼び出すと、次のようになります。

third returns to somewhere in second
second returns to somewhere in first

次に、thirdfourthを呼び出すと、次のようになります。

fourth returns to somewhere in third
third returns to somewhere in second
second returns to somewhere in first

各関数が呼び出されると、より多くの「返す場所」情報がどこかに格納される必要があります。

関数が戻る場合、その「戻る場所」情報が使用され、不要になります。たとえば、fourththirdのどこかに戻る場合、「どこに戻るか」情報の量は次のようになります。

third returns to somewhere in second
second returns to somewhere in first

基本的に; 「関数呼び出しセマンティクス」とは、次のことを意味します。

  • 「戻る場所」の情報が必要です
  • 情報の量は、関数が呼び出されると増加し、関数が戻ると減少します
  • 保存された最初の「戻る場所」情報は破棄された「戻る場所」情報の最後の部分になります

これは、FILO/LIFOバッファーまたはスタックを記述します。

あるタイプのツリーを使用しようとすると、ツリー内のすべてのノードが複数の子を持つことはありません。 注:複数の子を持つノードは、関数が2つ以上の関数を呼び出す場合にのみ発生します同時には、ある種の同時実行性(スレッド、フォークなど)を必要とします。 ()など)そして、それは「関数呼び出しセマンティクス」ではありません。ツリー内のすべてのノードが複数の子を持つことはありません。その「ツリー」は、FILO/LIFOバッファまたはスタックとしてのみ使用されます。そして、それはFILO/LIFOバッファまたはスタックとしてのみ使用されるため、「ツリー」はスタックであると主張するのは公正です(そして、唯一の違いはWordゲームや実装の詳細です)。

「関数呼び出しセマンティクス」を実装するために使用できると思われる他のデータ構造にも同じことが当てはまります。これはスタックとして使用されます(違いはWordゲームや実装の詳細です)。 「関数呼び出しセマンティクス」に違反しない限り。 注:できれば他のデータ構造の例を示しますが、少し妥当な他の構造は考えられません

もちろん、スタックの実装方法は実装の詳細です。これは、メモリの領域(「現在のスタックトップ」を追跡する場所)、ある種のリンクリスト(「リストの現在のエントリ」を追跡する場所)、または一部に実装できます。他の方法。ハードウェアにサポートが組み込まれているかどうかも関係ありません。

注:いずれかのプロシージャの呼び出しが一度にアクティブになる可能性がある場合。次に、「戻る場所」の情報にスペースを静的に割り当てることができます。これはまだスタックです(例:FILO/LIFOの方法で使用される静的に割り当てられたエントリのリンクされたリスト)

また、「関数呼び出しのセマンティクス」に従わないものがあることに注意してください。これらには、「潜在的に非常に異なるセマンティクス」(たとえば、継続渡し、アクターモデル)が含まれます。また、同時実行(スレッド、ファイバーなど)、setjmp/longjmp、例外処理などの「関数呼び出しセマンティクス」の一般的な拡張機能も含まれています。

6
Brendan

おもちゃの連結言語 [〜#〜] xy [〜#〜] は、実行にcall-queueとデータスタックを使用します。

すべての計算ステップでは、実行する次のWordをデキューするだけで、組み込みの場合は、その内部関数にデータスタックと呼び出しキューを引数として渡すか、ユーザー定義を使用して、それを構成する単語をキューの先頭にプッシュします。

したがって、最上位の要素を2倍にする関数があるとします。

; double dup + ;
// defines 'double' to be composed of 'dup' followed by '+'
// dup duplicates the top element of the data stack
// + pops the top two elements and Push their sum

次に、合成関数+およびdupには、次のスタック/キューtypシグネチャがあります。

// X is arbitraty stack, Y is arbitrary queue, ^ is concatenation
+      [X^a^b Y] -> [X^(a + b) Y]
dup    [X^a Y] -> [X^a^a Y]

そして逆説的に、doubleは次のようになります。

double [X Y] -> [X dup^+^Y]

したがって、ある意味で、XYはスタックレスです。