ネストされた関数呼び出しをインライン化できる場合、プログラムはなぜ呼び出しスタックを使用するのですか?


32

コンパイラに次のようなプログラムを取らせてはいけません。

function a(b) { return b^2 };
function c(b) { return a(b) + 5 };

それを次のようなプログラムに変換します。

function c(b) { return b^2 + 5 };

これにより、コンピュータがc(b)の返信先アドレスを覚える必要がなくなりますか?

プログラムを格納し、コンパイルをサポートするために必要なハードディスク領域とRAMの増加が(それぞれ)呼び出しスタックを使用する理由だと思います。あれは正しいですか?


30
意味のあるサイズのプログラムでこれを行うとどうなるかを確認してください。特に、関数は複数の場所から呼び出されます。
user253751

10
また、コンパイラーは、どの関数が呼び出されるのか分からない場合があります!愚かな例:window[prompt("Enter function name","")]()
user253751

26
function(a)b { if(b>0) return a(b-1); }スタックなしでどのように実装しますか?
pjc50

8
関数型プログラミングとの関係はどこにありますか?
マストフ

14
@ pjc50:末尾再帰であるため、コンパイラーはそれをmutableを持つループに変換しますb。しかし、要点は、すべての再帰関数が再帰を排除できるわけではなく、関数が原則として可能であっても、コンパイラーはそれを行うほど賢くないかもしれません。
スティーブジェソップ

回答:


75

これは「インライン化」と呼ばれ、多くのコンパイラは、理にかなっている場合に最適化戦略としてこれを行います。

特定の例では、この最適化によりスペースと実行時間の両方が節約されます。ただし、プログラム内の複数の場所で関数が呼び出された場合(珍しいことではありません!)、コードサイズが増加するため、戦略はより疑わしくなります。(そしてもちろん、関数がそれ自体を直接または間接的に呼び出した場合、コードのサイズが無限になるため、インライン化することはできません。)

そして明らかに、「プライベート」機能に対してのみ可能です。外部の呼び出し元に公開されている関数は、少なくとも動的リンクのある言語では最適化できません。


7
@Blrfl:最新のコンパイラーは、実際にはヘッダーに定義を必要としません。翻訳ユニット間でインライン化できます。ただし、これにはまともなリンカーが必要です。ヘッダーファイルの定義は、ダムリンカーの回避策です。
–MSalters

3
「外部発信者向けに公開されている関数は、最適化して離れることはできません」-関数は存在する必要がありますが、特定の呼び出しサイト(独自のコード、またはソースがある場合は外部発信者)をインライン化できます。
Random832

14
すごい、28はすべてのインライン化が不可能な理由に言及さえしていない答えに賛成です:再帰。
マストフ

3
@R ..:LTOはLINK Time Optimizationであり、LOAD Time Optimizationではありません。
–MSalters

2
@immibis:しかし、その明示的なスタックがコンパイラによって導入された場合、そのスタック呼び出しスタックです。
user2357112は

51

あなたの質問には2つの部分があります:なぜ(関数呼び出しを定義で置き換えるのではなく)複数の関数があるのか​​、なぜそれらの関数を他のどこかにデータを静的に割り当てるのではなく、呼び出しスタックで実装するのですか?

最初の理由は再帰です。「このリスト内のすべてのアイテムに対して新しい関数呼び出しを行いましょう」という種類だけでなく、他の多くの関数を間に置いて、2つの関数呼び出しを同時にアクティブにする控えめな種類もあります。これをサポートするには、スタックにローカル変数を配置する必要があり、一般に再帰関数をインライン化することはできません。

次に、ライブラリに問題があります。どの関数がどこからどのくらいの頻度で呼び出されるかわからないため、「ライブラリ」は実際にはコンパイルできず、すべてのクライアントに便利な高レベル形式でしか出荷されません。アプリケーションにインライン化されました。これに関する他の問題は別として、すべての利点を備えた動的リンクを完全に失います。

さらに、可能な場合でも関数をインライン化しない理由は多数あります。

  1. 必ずしも高速ではありません。スタックフレームをセットアップして破棄するのは、実行時間の0.1%でさえない多くの大きな関数またはループ関数のための、おそらく1ダースのシングルサイクル命令です。
  2. 遅いかもしれません。コードの複製にはコストがかかります。たとえば、命令キャッシュにより多くのプレッシャーがかかります。
  3. 一部の関数は非常に大きく、多くの場所から呼び出され、どこでもインライン化すると、バイナリが妥当な範囲をはるかに超えて増加します。
  4. 多くの場合、コンパイラは非常に大きな関数を使用するのに苦労します。他のすべてが等しい場合、サイズ2 * Nの関数は2 * T時間以上かかり、サイズNの関数はT時間かかります。

1
私はポイント4に驚いています。この理由は何ですか?
ジャックB

12
@JacquesB多くの最適化アルゴリズムは、2次、3次、または技術的にはNP完全です。標準的な例は、レジスタの割り当てです。これは、グラフの色付けとの類推によりNP完全です。(通常、コンパイラーは正確な解決策を試みませんが、ほんのわずかな非常に貧弱なヒューリスティックのみが線形時間で実行されます。) n log n time with n basic blocks)。

2
「ここには本当に2つの質問があります」いいえ、私はしません。ただ1つ-コンパイラが呼び出された関数のコードで置き換えるなど、単なるプレースホルダーとして関数呼び出しを扱わないのはなぜですか?
moonman239

4
@ moonman239それからあなたの言葉遣いは私を投げた。それでも、あなたの質問私の答えのように分解することでき、それは有用な視点だと思います。

16

スタックを使用すると、有限数のレジスタによって課される制限をエレガントにバイパスできます。

正確に26個のグローバル「レジスタa〜z」(または8080チップの7バイトサイズのレジスタのみ)を想像してください。このアプリで作成するすべての関数は、このフラットリストを共有します。

素朴なスタートは、最初のいくつかのレジスタを最初の関数に割り当て、それが3つしかかからないことを知って、2番目の関数の「d」から始めます...すぐに使い果たされます。

代わりに、チューリングマシンのような比phor的なテープがある場合は、使用しているすべての変数を保存してテープをforward()することにより、各関数が「別の関数を呼び出す」ようにできます。必要に応じて登録します。呼び出し先が終了すると、制御を親関数に返します。親関数は、必要に応じて呼び出し先の出力をどこに取り込むかを知っており、テープを逆方向に再生してその状態を復元します。

基本的な呼び出しフレームはそれだけであり、コンパイラが1つの関数から別の関数への遷移の前後に配置する標準化されたマシンコードシーケンスによって作成および削除されます。(Cスタックフレームを思い出さなければならないのは久しぶりですが、X86_calling_conventions誰が何を落とすのかというさまざまな方法を読むことができます。)

(再帰は素晴らしいですが、スタックなしでレジスタをジャグリングしなければならなかったなら、本当にスタックに感謝するでしょう。)


プログラムを格納し、コンパイルをサポートするために必要なハードディスク領域とRAMの増加が(それぞれ)呼び出しスタックを使用する理由だと思います。あれは正しいですか?

最近はインライン化できますが(ビデオストリームの世界では、「高速」は常に優れています。「アセンブリの数KBを少なくする」という意味はほとんどありません)、主な制限は、特定のタイプのコードパターンでフラット化するコンパイラーの能力にあります。

たとえば、ポリモーフィックオブジェクト-渡されるオブジェクトの唯一のタイプがわからない場合、フラット化できません。オブジェクトのvtableの機能を見て、そのポインターを介して呼び出す必要があります。実行時に行うのは簡単で、コンパイル時にインライン化することは不可能です。

最新のツールチェーンは、objのフレーバーがどれであるかを正確に知るのに十分な呼び出し元をフラット化したときに、多相的に定義された関数を喜んでインライン化できます。

class Base {
    public: void act() = 0;
};
class Child1: public Base {
    public: void act() {};
};
void ActOn(Base* something) {
    something->act();
}
void InlineMe() {
    Child1 thingamabob;
    ActOn(&thingamabob);
}

上記では、コンパイラーは、act()の内部にあるものを介してInlineMeから静的にインライン化することを選択することも、実行時にvtableに触れる必要もありません。

ただし、オブジェクトのフレーバーの不確実性により、同じ関数の他の呼び出しインライン化されている場合でも、離散関数への呼び出しとして残されます。


11

そのアプローチが処理できない場合:

function fib(a) { if(a>2) return fib(a-1)+fib(a-2); else return 1; }

function many(a) { for(i = 1 to a) { b(i); };}

あります限られた、あるいは全くコールスタックを持つ言語やプラットフォームが。PICマイクロプロセッサには、2〜32エントリに制限されたハードウェアスタックがあります。これにより、設計上の制約が作成されます。

COBOLは再帰を禁止します:https : //stackoverflow.com/questions/27806812/in-cobol-is-it-possible-to-recursively-call-a-paragraph

再帰の禁止を課すということは、プログラムのコールグラフ全体をDAGとして静的に表現できることを意味します。コンパイラは、戻り値の代わりに固定ジャンプで呼び出された場所ごとに関数のコピーを1つ出力できます。スタックは不要で、プログラムスペースが増えるだけで、複雑なシステムにはかなりの容量が必要になる可能性があります。しかし、小さな組み込みシステムの場合、これは、実行時にスタックオーバーフローが発生しないことを保証できることを意味します。これは、原子炉/ジェットタービン/車のスロットル制御などにとって悪いニュースです。


12
最初の例は基本的な再帰であり、あなたはそこで正しいです。しかし、2番目の例は、別の関数を呼び出すforループのようです。関数のインライン化は、ループの展開とは異なります。ループを展開せずに関数をインライン化できます。または、微妙な詳細を見逃していませんか?
jpmc26

1
最初の例がフィボナッチ数列を定義することを意図している場合、それは間違っています。(fib呼び出しがありません。)
パエロエベルマン

1
再帰を禁止することは、コールグラフ全体をDAGとして表すことができることを意味しますが、入れ子になったコールシーケンスの完全なリストを適切なスペースにリストできるという意味ではありません。128KBのコードスペースを備えたマイクロコントローラーの私の1つのプロジェクトで、最大パラメーターRAM要件に影響を与える可能性のあるすべての関数を含むコールグラフを要求するというミスを犯しました。完全なコールグラフはさらに長く、128Kのコードスペースに収まるプログラム用でした。
-supercat

8

関数のインライン化が必要であり、ほとんどの(最適化)コンパイラーがそれを行っています。

概念的には呼び出された関数の書き換えで呼び出しを置き換えるため、インライン化では呼び出された関数が既知である必要があります(呼び出された関数が大きすぎない場合にのみ有効です)。一般的ではないインライン未知の機能(例えば、関数ポインタ-そしてから機能含んでいるあなたはとても動的にリンクされた 共有ライブラリ -一部では仮想メソッドとしておそらく見える、vtableのを、しかし、いくつかのコンパイラは時々かもしれないスルー最適化devirtualizationの技術)。もちろん、再帰関数をインライン化することは常に可能というわけではありません(一部の賢いコンパイラーは部分評価を使用し場合によっては再帰関数をインライン化できるかもしれません)。

インライン化は、たとえ簡単に可能であるとしても、必ずしも効果的ではないことにも注意してください:あなた(実際にはコンパイラー)は、CPUキャッシュ(または分岐予測子)の効率が低下するほどコードサイズが大きくなり、プログラムが実行される可能性がありますもっとゆっくり。

あなたはあなたの質問をそのようにタグ付けしたので、関数型プログラミングスタイルに少し焦点を当てています

呼び出しスタックは必要ないことに注意してください(少なくとも「呼び出しスタック」式のマシンの意味では)。ヒープのみを使用できます。

したがって、継続見て、継続渡しスタイル(CPS)およびCPS変換について詳しく読んでください(直感的に、ヒープに割り当てられた具体的な「呼び出しフレーム」として継続クロージャーを使用できます。効率的なガベージコレクタが必要です)。

Andrew Appelは、Compiling with Continuationsという本を書きました。古いペーパーガベージコレクションは、スタック割り当てよりも高速です。参照してくださいA.Kennedyの論文(ICFP2007)継続継続はしてコンパイル、

QueinnecのLisp In Small Piecesの本も読むことをお勧めします。この本には、継続と編集に関連するいくつかの章があります。

また、一部の言語(例:Brainfuck)または抽象マシン(例:OISCRAM)には呼び出し機能はありませんが、チューリング完全であるため、(理論上)関数呼び出しメカニズムは必要ありません。とても便利です。ところで、いくつかの古い命令セットアーキテクチャ(IBM / 370など)には、ハードウェアコールスタックやプッシュコールマシン命令さえありません(IBM / 370にはブランチとリンクのマシン命令しかありませんでした)

最後に、プログラム全体(必要なすべてのライブラリを含む)に再帰がない場合、各関数の戻りアドレス(および「ローカル」変数)を静的な場所に格納できます。古いFortran77コンパイラは1980年代初期にそれを行いました(したがって、コンパイルされたプログラムはその時点で呼び出しスタックを使用しませんでした)。


2
CPSには「呼び出しスタック」がないため、非常に議論の余地があります。スタックにはありません。通常のRAMの神秘的な領域であり、ハードウェアサポート%espなどが少しありますが、RAMの別の領域にある適切な名前のスパゲッティスタックで同等の簿記を保持します。特に、返信先アドレスは継続的に基本的にエンコードされます。そしてもちろん、インライン化によってまったく呼び出しを行わないよりも継続は速くありません(そして、これがOPに到達したようです)。

Appelの古い論文は、CPSがコールスタックを持つのと同じくらい高速であると主張しました(そしてベンチマークで実証されました)。
バジルスタリンケビッチ

私はそれについて懐疑的ですが、それは私が主張したものではありません。

1
実際、これは1980年代後半のMIPSワークステーションでした。おそらく、現在のPCのキャッシュ階層により、パフォーマンスが若干異なります。いくつかのアペルの主張を分析する論文(そして実際、現在のマシン上で、スタック割り当てがあるかもしれないが行われてきた少し慎重に細工されたガベージコレクションより数percents- -by速い)
バジーレStarynkevitch

1
@Gilles:Cortex M0やM3(およびおそらくM4など)のような新しいARMコアの多くは、割り込み処理などのハードウェアスタックをサポートしています。さらに、Thumb命令セットには、LRを含む/含まないR0-R7の任意の組み合わせを持つSTRMDB R13、およびPCを含む/含まないR0-R7の任意の組み合わせのLDRMIA R13を含むSTRM / STRM命令の限定サブセットが含まれます。スタックポインターとしてのR13。
-supercat

8

インライン化(​​関数呼び出しを同等の機能に置き換える)は、小さな単純な関数の最適化戦略としてうまく機能します。関数呼び出しのオーバーヘッドは、プログラムサイズの追加によるわずかなペナルティ(または場合によってはペナルティなし)と効果的にトレードオフできます。

ただし、他の関数を順番に呼び出す大きな関数は、すべてがインライン化されていると、プログラムサイズが大幅に増大する可能性があります。

呼び出し可能関数のポイントは、プログラマーだけでなく、マシン自体によっても効率的な再利用を促進することであり、それには合理的なメモリやディスク上のフットプリントなどのプロパティが含まれます。

価値のあることについては、呼び出しスタックなしで呼び出し可能な関数を使用できます。例:IBM System / 360。そのハードウェアでFORTRANなどの言語でプログラミングする場合、プログラムカウンター(戻りアドレス)は、関数のエントリポイントの直前に予約されているメモリの小さなセクションに保存されます。再利用可能な関数は使用できますが、再帰またはマルチスレッドコードは使用できません(再帰的または再入可能な呼び出しを試みると、以前に保存されたリターンアドレスが上書きされます)。

他の回答で説明したように、スタックは良いものです。これらは、再帰呼び出しとマルチスレッド呼び出しを容易にします。再帰を使用するようにコーディングされたアルゴリズムは、再帰に依存せずにコーディングできますが、結果はより複雑になり、保守が難しくなり、効率が低下する可能性があります。スタックレスアーキテクチャがマルチスレッドをサポートできるかどうかはわかりません。

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