呼び出しスタックに静的な最大サイズがあるのはなぜですか?


46

いくつかのプログラミング言語で作業してきたので、必要に応じて自動的に拡張するのではなく、スレッドスタックが事前に定義された最大サイズを持っている理由を常に疑問に思っていました。 

それに比べて、ほとんどのプログラミング言語に見られる特定の非常に一般的な高レベル構造(リスト、マップなど)は、必要に応じて成長するように設計されていますが、新しい要素が追加され、利用可能なメモリまたは計算の制限によってサイズが制限されます(例:32ビットアドレス指定)。

しかし、最大スタックサイズがデフォルトまたはコンパイラオプションによって事前に制限されていないプログラミング言語やランタイム環境については知りません。これが、プロセスで利用可能なメモリの最小割合だけがスタックに使用されている場合でも、過度の再帰がユビキタススタックオーバーフローエラー/例外を非常に迅速に引き起こす理由です。

ほとんどの(すべてではないにしても)ランタイム環境が、実行時にスタックが成長できるサイズの最大制限を設定するのはなぜですか?


13
この種のスタックは連続したアドレス空間であり、背後で静かに移動することはできません。アドレス空間は32ビットシステムで貴重です。
CodesInChaos

7
アカデミアから漏れる再帰のような象牙の塔のアイデアの発生を減らし、コードの可読性の低下や総所有コストの増加などの現実の世界で問題を引き起こします;)
ブラッドトーマス

6
@BradThomasそれがテールコール最適化の目的です。
JAB

3
@JohnWu:今と同じこと、ほんの少し後:メモリ不足。
ヨルグWミットタグ

1
場合には、それは明らかではないが、メモリ不足を理由の一つは、スタックを使い果たすよりも悪いスタックを使い果たす(トラップページがありますと仮定すると)のみ引き起こすことがあるあなたが失敗するプロセスを。メモリを使い果たすと、次にメモリ割り当てを行おうとすると、何でも失敗する可能性があります。この場合も、トラップページまたはスタック外を検出する他の手段を備えていないシステムでは、スタックを使い果たすと致命的となり、未定義の動作が発生する可能性があります。このようなシステムでは、フリーストアメモリを使い果たしてしまい、無制限の再帰でコードを書くことはできません。
スティーブジェソップ

回答:


13

スタックがアドレス空間で連続している必要のないオペレーティングシステムを記述することは可能です。基本的に、次のことを保証するために、呼び出し規約でいくつかの余分な混乱が必要です。

  1. 呼び出している関数の現在のスタックエクステントに十分なスペースがない場合は、新しいスタックエクステントを作成し、呼び出しの一部としてスタックポインターを移動してその開始を指します。

  2. その呼び出しから戻ると、元のスタックエクステントに戻ります。ほとんどの場合、同じスレッドで将来使用するために(1)で作成したものを保持します。原則としてそれを解放できますが、その方法は、ループ内で境界を越えて前後にホッピングを続け、すべての呼び出しにメモリ割り当てが必要な、かなり非効率的なケースです。

  3. setjmpおよびlongjmp、またはOSがローカル以外の制御権を移動するために持っている同等の機能はすべて動作中であり、必要に応じて古いスタック範囲に正しく戻ることができます。

私は「呼び出し規約」と言います-具体的には、おそらく呼び出し元ではなく関数プロローグで行うのが最善だと思いますが、これに関する私の記憶はかすんでいます。

かなりの数の言語がスレッドの固定スタックサイズを指定する理由は、これを行わない OS上でネイティブスタックを使用して作業したいからです。他のみんなの答えが言うように、各スタックはアドレス空間で連続している必要があり、移動できないという前提の下で、各スレッドが使用する特定のアドレス範囲を予約する必要があります。つまり、事前にサイズを選択する必要があります。アドレス空間が巨大で、選択したサイズが非常に大きい場合でも、2つのスレッドがあるとすぐに選択する必要があります。

「あは」と言います、「これらの連続していないスタックを使用するOSとは何ですか?私には役に立たない曖昧な学術システムだと思います!」さて、それは別の質問ですが、幸いなことにすでに質問と回答があります。


36

通常、これらのデータ構造には、OSスタックにはないプロパティがあります。

  • リンクリストには、連続したアドレススペースは必要ありません。そのため、成長したときにどこからでもメモリを追加できます。

  • C ++のベクターのような連続したストレージを必要とするコレクションでさえ、OSスタックよりも利点があります。つまり、成長するたびにすべてのポインター/イテレーターを無効と宣言できます。一方、OSスタックは、ターゲットが属するフレームの関数が戻るまで、スタックへのポインターを有効に保つ必要があります。

プログラミング言語またはランタイムは、OSスタックの制限を回避するために、非連続または移動可能な独自のスタックを実装することを選択できます。Golangは、このようなカスタムスタックを使用して非常に多数のコルーチンをサポートします。これらは、もともと不連続メモリとして実装されていましたが、ポインタートラッキングのおかげで可動スタックを介して実装されています(hobbのコメントを参照)。Stackless python、Lua、Erlangもカスタムスタックを使用する可能性がありますが、私はそれを確認しませんでした。

64ビットシステムでは、アドレススペースが十分にあり、物理メモリは実際に使用するときにのみ割り当てられるため、比較的低コストで比較的大きなスタックを構成できます。


1
これは良い答えであり、私はあなたの意味に従いますが、各メモリユニットは独自のアドレスを持っているので、用語は「連続」ではなく「連続」メモリブロックではありませんか?
じめじめした

2
「コールスタックを制限する必要がない」ための+1シンプルさとパフォーマンスのためにそのように実装されることがよくありますが、そうである必要はありません。
ポールドレーパー

あなたはGoについてかなり正しい。実際、私の理解では、古いバージョンには不連続なスタックがあり、新しいバージョンには可動スタックがあります。いずれにしても、多数のゴルーチンを許可する必要があります。スタックにゴルーチンごとに数メガバイトを事前に割り当てると、コストがかかりすぎて目的を適切に果たすことができません。
ホッブズ

@hobbs:はい、Goは成長可能なスタックから始めましたが、それらを高速にするのは困難でした。Goが正確なガベージコレクターを取得すると、移動可能なスタックを実装するために移動します。スタックが移動すると、正確な型マップを使用して前のスタックへのポインターが更新されます。
マチューM.

26

実際には、スタックを増やすことは困難です(時には不可能です)。理由を理解するには、仮想メモリをある程度理解する必要があります。

シングルスレッドアプリケーションと連続メモリのYe Olde Daysでは、3つがプロセスアドレススペースの3つのコンポーネントでした。コード、ヒープ、スタックです。これら3つのレイアウト方法はOSによって異なりますが、一般的にはメモリの一番下からコードが最初に来て、次にヒープが上に上がり、スタックがメモリの一番上から始まり、下に向かって成長しました。オペレーティングシステム用に予約されたメモリもありましたが、無視できます。当時のプログラムでは、スタックがかなり劇的にオーバーフローしていました。スタックがヒープにクラッシュし、最初に更新されたものに応じて、不良データを処理するか、サブルーチンからメモリの任意の部分に戻ります。

メモリ管理はこのモデルを多少変更しました。プログラムの観点からは、プロセスメモリマップの3つのコンポーネントがまだあり、一般的に同じ方法で編成されていましたが、各コンポーネントは独立したセグメントとして管理され、MMUはプログラムがセグメント外のメモリにアクセスしようとした場合のOS。仮想メモリを取得すると、プログラムにそのアドレス空間全体へのアクセスを許可する必要も希望もありませんでした。そのため、セグメントには固定境界が割り当てられました。

それでは、なぜプログラムにその完全なアドレス空間へのアクセスを許可することが望ましくないのでしょうか?そのメモリはスワップに対する「コミットチャージ」を構成するためです。いつでも、あるプログラムのメモリの一部またはすべてを、別のプログラムのメモリ用のスペースを確保するためにスワップに書き込む必要があります。すべてのプログラムが潜在的に2GBのスワップを消費する可能性がある場合、すべてのプログラムに十分なスワップを提供するか、2つのプログラムが必要以上にスワップを必要とする可能性があります。

この時点で、十分な仮想アドレス空間を想定して、必要に応じてこれらのセグメントを拡張できます。実際、データセグメント(ヒープ)は時間とともに成長します。小さなデータセグメントから始め、それが必要です。この時点で、単一のスタックで、スタックセグメントを拡張することは物理的に可能でした。OSは、セグメントの外に何かをプッシュしてメモリを追加しようとする試みをトラップできます。しかし、これも特に望ましくありません。

マルチスレッドを入力します。この場合、各スレッドには、固定サイズの独立したスタックセグメントがあります。しかし、今では仮想アドレス空間にセグメントが次々とレイアウトされているため、別のセグメントを移動せずにセグメントを拡張する方法はありません-プログラムには潜在的にスタックにあるメモリへのポインタがあるため、これはできません。あるいは、セグメント間にいくつかのスペースを残すこともできますが、ほとんどの場合、そのスペースは無駄になります。より良いアプローチは、アプリケーション開発者に負担をかけることでした。本当に深いスタックが必要な場合は、スレッドの作成時にそれを指定できます。

現在、64ビットの仮想アドレス空間を使用して、事実上無限の数のスレッドに対して事実上無限のスタックを作成できました。ただし、これも特に望ましいことではありません。ほとんどすべての場合、スタックが低すぎるということは、コードのバグを示しています。1 GBのスタックを提供すると、そのバグの発見が延期されます。


3
現在のx86-64 CPUは唯一のアドレス空間の48ビット持っている
CodesInChaos

Afaik、Linux スタックを動的に拡張します。プロセスが現在割り当てられているスタックのすぐ下の領域にアクセスしようとすると、プロセスはセグメンテーション違反ではなく、スタックメモリの追加ページをマッピングするだけで割り込みが処理されます。
cmaster

2
@cmaster:true。ただし、kdgregoryが「スタックを成長させる」という意味ではありません。スタックとして使用するために現在指定されているアドレス範囲があります。必要に応じて、より多くの物理メモリをそのアドレス範囲に徐々にマッピングすることについて話しています。kdgregoryは、範囲を拡大することは困難または不可能だと言っています。
スティーブジェソップ

x86は唯一のアーキテクチャではなく、48ビットは事実上無限です
kdgregory

1
ところで、主にセグメンテーションに対処する必要があるため、x86での作業はあまり面白くないと思います。MC68kプラットフォームでのプロジェクトを非常に好みました;-)
kdgregory

4

最大サイズが固定されたスタックは、どこにでもありません。

また、正しく取得することは困難です。スタックの深さはべき乗則分布に従うため、スタックサイズをどれだけ小さくしても、スタックがさらに小さい場合でも関数のかなりの部分が残ります(つまり、スペースが無駄になります)。どれだけ大きくしても、さらに大きなスタックを持つ関数が存在します(したがって、エラーのない関数に対して強制的にスタックオーバーフローエラーが発生します)。言い換えると、選択したサイズに関係なく、常に同時に大きすぎて小さすぎます。

スタックを小さく開始して動的に成長させることで最初の問題を修正できますが、それでも2番目の問題があります。とにかくスタックが動的に成長するのを許可する場合、なぜそれに任意の制限を設定しますか?

たとえば、Erlang、Go、Smalltalk、Schemeなど、スタックが動的に成長し、最大サイズを持たないシステムがあります。そのようなものを実装する方法はたくさんあります。

  • 移動可能なスタック:途中に他の何かがあるために隣接するスタックがそれ以上成長できない場合は、メモリ内の別の場所に移動し、空きスペースを増やします
  • 不連続スタック:スタック全体を単一の連続したメモリスペースに割り当てる代わりに、複数のメモリスペースに割り当てます。
  • ヒープに割り当てられたスタック:スタックとヒープに別々のメモリ領域を持たせる代わりに、単にスタックをヒープに割り当てます。あなたが気づいたように、ヒープに割り当てられたデータ構造は、必要に応じて成長したり縮小したりする問題はありません
  • スタックをまったく使用しないでください。これはオプションでもあります。たとえば、スタック内の関数の状態を追跡する代わりに、関数が呼び出し先に継続を渡すようにします。

強力な非ローカル制御フロー構造があるとすぐに、単一の連続したスタックのアイデアはとにかく窓から消えます。たとえば、再開可能な例外と継続はスタックを「フォーク」するため、実際にはネットワークになります。スタック(例:スパゲッティスタックで実装)。また、Smalltalkのような一流の変更可能なスタックを備えたシステムには、スパゲッティスタックなどが必要です。


1

OSは、スタックが要求されたときに連続したブロックを提供する必要があります。それができる唯一の方法は、最大サイズが指定されている場合です。

たとえば、リクエスト中にメモリが次のようになったとします(Xは使用済み、Oは未使用を表します):

XOOOXOOXOOOOOX

スタックサイズ6のリクエストの場合、OSの答えは、6を超えるサイズが利用できる場合でも、noと答えます。サイズ3のスタックの要求の場合、OSの答えは、3つの空のスロット(O)が連続した領域の1つになります。

また、次の連続するスロットが占有されている場合、成長を許可することの難しさがわかります。

言及されている他のオブジェクト(リストなど)はスタックに置かれず、非連続または断片化された領域のヒープに配置されるため、成長するときにスペースを取得するだけで、連続する必要はありません異なった管理。

ほとんどのシステムは、スタックサイズに適切な値を設定します。より大きなサイズが必要な場合は、スレッドの構築時にそれをオーバーライドできます。


1

Linuxでは、これは純粋にリソース制限であり、暴走したプロセスが有害な量のリソースを消費する前に強制終了します。私のDebianシステムでは、次のコード

#include <sys/resource.h>
#include <stdio.h>

int main() {
    struct rlimit limits;
    getrlimit(RLIMIT_STACK, &limits);
    printf("   soft limit = 0x%016lx\n", limits.rlim_cur);
    printf("   hard limit = 0x%016lx\n", limits.rlim_max);
    printf("RLIM_INFINITY = 0x%016lx\n", RLIM_INFINITY);
}

出力を生成します

   soft limit = 0x0000000000800000
   hard limit = 0xffffffffffffffff
RLIM_INFINITY = 0xffffffffffffffff

ハード制限はに設定されていることに注意してくださいRLIM_INFINITY:プロセスは、そのソフト制限を任意の量に上げることができます。ただし、プログラマーが異常な量のスタックメモリを本当に必要とすると信じる理由がない限り、8メビバイトのスタックサイズを超えるとプロセスは強制終了されます。

この制限により、暴走プロセス(意図しない無限再帰)は、大量のメモリを消費し始める前に長時間強制終了され、システムが強制的にスワップを開始します。これにより、クラッシュしたプロセスとクラッシュしたサーバーの違いが生じる可能性があります。ただし、大規模なスタックを正当に必要とするプログラムを制限するものではなく、ソフト制限を適切な値に設定するだけです。


技術的には、スタックは動的に成長します。ソフト制限が8メビバイトに設定されている場合、この量のメモリがまだ実際にマップされているという意味ではありません。ほとんどのプログラムは、それぞれのソフト制限に近づかないため、これはかなり無駄です。むしろ、カーネルはスタックの下のアクセスを検出し、必要に応じてメモリページにマップします。したがって、スタックサイズの唯一の実際の制限は、64ビットシステムで使用可能なメモリです(アドレススペースの断片化は、16ゼビバイトのアドレススペースサイズではかなり理論的です)。


2
これは、最初のスレッドのみのスタックです。新しいスレッドは新しいスタックを割り当てる必要があり、他のオブジェクトに実行されるため制限されます。
ザンリンクス

0

最大のものであるため、スタックサイズは静的であるの定義は、「最大」。あらゆる種類の最大値は、固定され、合意された制限値です。自発的に移動するターゲットとして動作する場合、最大値ではありません。

仮想メモリオペレーティングシステム上のスタックは、実際には最大で動的に成長します

そういえば、静的である必要はありません。むしろ、プロセスごとまたはスレッドごとに構成することもできます。

質問がある場合は、「なぜです(通常ははるかに少ない使用可能なメモリよりも、人為的に課さ1)最大スタックサイズはありますか」?

1つの理由は、ほとんどのアルゴリズムが膨大な量のスタックスペースを必要としないことです。大きなスタックは、暴走の可能性のある再帰を示しています。使用可能なすべてのメモリを割り当てる前に、暴走再帰を停止することをお勧めします。暴走再帰のように見える問題は、おそらく予期しないテストケースによって引き起こされる縮退スタックの使用です。たとえば、バイナリ内置演算子のパーサーが右オペランドで再帰することによって機能するとします。最初のオペランドを解析、演算子をスキャン、残りの式を解析します。これは、スタックの深さが式の長さに比例することを意味しますa op b op c op d ...。この形式の巨大なテストケースには、巨大なスタックが必要になります。適切なスタック制限に達したときにプログラムを中止すると、これをキャッチします。

最大スタックサイズが固定されているもう1つの理由は、そのスタックの仮想スペースが特別な種類のマッピングを介して予約できるため、保証されることです。保証は、スペースが別の割り当てに割り当てられず、スタックが制限に達する前にそれと衝突することを意味します。このマッピングを要求するには、最大スタックサイズパラメータが必要です。

これと同様の理由で、スレッドには最大スタックサイズが必要です。それらのスタックは動的に作成され、何かと衝突した場合は移動できません。仮想スペースは事前に予約する必要があり、その割り当てにはサイズが必要です。


@Lynn最大サイズが静的である理由を尋ねませんでした。(s)事前に定義された理由を尋ねました。
ウィルカルダーウッド
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.