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


19

通常、関数呼び出しは通常スタックを使用して実装されることを知っています。フレーム、リターンアドレス、パラメータ、ロットがあります。

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

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

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

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

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


Cleanは、グラフとグラフ書き換えマシンを使用します。これは、3スタックマシンを使用して実装されますが、通常とは内容が異なります。

仮想マシンの場合、リンクリストを使用できます。リストの各ノードはフレームです。ハードウェアスタックはVMによって使用されるため、これにより、オーバーヘッドがなくてもフレームをヒープ上に存在させることができますrealloc()
shawnhcorey 16

回答:


19

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

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

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

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

私の知る限り、MITのハッカーコミュニティが1つを配信し、PUSHJ(プッシュリターンアドレスとジャンプ)操作を発見したとき、コールスタックハードウェアを普及させたのはPDP-6(DEC-10に成長しました)でした10進印刷ルーチンを50命令から10命令に減らすことができました。

再帰を許可する言語の最も基本的な関数呼び出しセマンティクスには、スタックとうまく一致する機能が必要です。必要なのがそれだけなら、基本的なスタックは適切で単純な一致です。それ以上必要な場合は、データ構造でさらに処理する必要があります。

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

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


2
それは素晴らしい歴史的洞察でした、ありがとう!私が質問をしたとき、私は確かに継続、特に継続通過スタイル(CPS)を念頭に置いていました。その場合、スタックは不便であるだけでなく、おそらく必要ではありません。どこに戻るかを覚える必要はなく、実行を継続する場所を提供します。他のスタックレスアプローチが一般的かどうか疑問に思いましたが、あなたは私が知らない非常に良いアプローチをいくつか与えました。
ロレンツォデマテ

少し関連している:「言語が再帰を許可しない場合」を正しく指摘しました。再帰、特に末尾再帰ではない関数についてはどうでしょうか?「設計による」スタックが必要ですか?
ロレンツォデマテ

「呼び出しスタックは、再帰または相互再帰を許可する言語でのみ必要です」-いいえ。関数が複数の場所(例えば、両方から呼び出すことができればfoobar呼んでもbaz)、その関数はに戻るには何を知っている必要があります。この「誰に戻るか」の情報をネストすると、スタックになります。あなたがそれを何と呼んでも、CPUのハードウェアやソフトウェアでエミュレートするものによってサポートされていても(または静的に割り当てられたエントリのリンクリストであっても)、それは依然としてスタックです。
ブレンダン

@Brendanは必ずしもそうではありません(少なくとも、それが私の質問の全体的な目的です)。「どこに戻るか」または「次にどこに行くか」は、スタック、つまりLIFO構造である必要がありますか?ツリー、マップ、キュー、または他の何かでしょうか?
ロレンツォデマテ

例えば、私の直感では、CPSにはツリーが必要なだけですが、確信が持てず、どこを見るべきかわかりません。それは私が求めています理由です...
ロレンツォDematté

6

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

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

互いに呼び出し可能な多くの関数があると想像してください。実行時に、各関数は、関数の終了時にどこに戻るかを知っている必要があります。first呼び出す場合はsecond次のとおりです。

second returns to somewhere in first

次に、もしsecond通話thirdあなたが持っています:

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

次に、もしthird通話fourthあなたが持っています:

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

各関数が呼び出されると、より多くの「戻り先」情報をどこかに保存する必要があります。

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

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

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

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

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

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

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

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

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

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


定義上、スタックはLIFOコレクションです。最後に、最初に。キューはFIFOコレクションです。
ジョンR.ストローム

スタックは唯一の許容可能なデータ構造ですか?もしそうなら、なぜですか?
ロレンツォデマッテ

@ JohnR.Strohm:修正済み:
ブレンダン

1
再帰のない言語(直接またはミュータル)では、メソッドが最後に呼び出された場所を識別する変数を各メソッドに静的に割り当てることができます。リンカがそのようなことを認識している場合、静的に実行可能なすべての実行パスが実際に使用された場合にスタックが行うことより悪くない方法で、そのような変数を割り当てることができます。
supercat 14年

4

おもちゃの連結言語XYは、実行のために呼び出しキューとデータスタックを使用します。

すべての計算ステップは、実行される次の単語を単に求め、組み込み関数の場合、内部関数にデータスタックとcall-queueを引数として渡すか、userdefを使用して、それを構成する単語をキューの先頭にプッシュするだけです。

したがって、上部の要素を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

次に、構成関数は、次のスタック/キューtypシグネチャ+dup持ちます。

// 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はスタックレスです。


わあ、ありがとう!私はそれを調べます...それが本当に関数呼び出しに適用されるかは
わかりませ

1
@ Karl Damgaard Asmussen「それを構成する言葉をキューの前に押し出す」「前に押し出す」それはスタックではありませんか?

@ guesttttttt222222222はそうではありません。呼び出しスタックは戻りポインターを保管し、関数が戻ると、呼び出しスタックがポップされます。実行キューには関数へのポインターのみが格納され、次の関数を実行すると、その定義に展開されてキューの先頭にプッシュされます。XYでは、実行キューは実際には両端キューになります。これは、実行キューの背面でも機能する操作があるためです。
カールダンガードアスムッセン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.