Ayant travaillé avec quelques langages de programmation, je me suis toujours demandé pourquoi la pile de threads avait une taille maximale prédéfinie, au lieu de se développer automatiquement selon les besoins.
En comparaison, certaines structures de haut niveau très courantes (listes, cartes, etc.) que l'on trouve dans la plupart des langages de programmation sont conçues pour croître en fonction des besoins, tandis que de nouveaux éléments sont ajoutés, leur taille étant limitée uniquement par la mémoire disponible ou par des limites de calcul ( par exemple, adressage 32 bits).
Je ne connais cependant aucun langage de programmation ou environnement d'exécution dans lequel la taille maximale de la pile n'est pas pré-limitée par une option par défaut ou par le compilateur. C'est pourquoi trop de récursivité entraîne très rapidement une erreur / exception de débordement de pile omniprésente, même lorsque seul un pourcentage minimal de la mémoire disponible pour un processus est utilisé pour la pile.
Pourquoi la plupart des environnements d'exécution (sinon tous) définissent-ils une limite maximale pour la taille d'une pile pouvant croître au moment de l'exécution?
Réponses:
Il est possible d'écrire un système d'exploitation qui n'exige pas que les piles soient contiguës dans l'espace d'adressage. En gros, vous avez besoin de déconner davantage dans la convention d’appel pour vous assurer que:
s'il n'y a pas assez d'espace dans l'étendue de pile actuelle pour la fonction que vous appelez, vous créez une nouvelle étendue de pile et déplacez le pointeur de pile pour qu'il pointe vers le début de celui-ci dans le cadre de l'appel.
lorsque vous revenez de cet appel, vous revenez à l'étendue de pile d'origine. Très probablement, vous conservez celui créé à (1) pour une utilisation future par le même fil. En principe, vous pouvez le libérer, mais dans ce cas, vous rencontrez des problèmes plutôt inefficaces dans lesquels vous continuez à parcourir la limite en boucle, et chaque appel nécessite une allocation de mémoire.
setjmp
etlongjmp
, ou l’équivalent de votre système d’exploitation pour le transfert de contrôle non local, sont actifs et peuvent revenir correctement à l’étendue de la pile lorsque cela est nécessaire.Je dis "convention d'appel" - pour être précis, je pense que c'est probablement mieux de le faire par un prologue de fonction que par l'appelant, mais je me souviens très mal de cela.
La raison pour laquelle de nombreuses langues spécifient une taille de pile fixe pour un thread est qu'elle souhaite travailler avec la pile native, sur des systèmes d'exploitation qui ne le font pas . Comme le disent les réponses de tous les autres, en supposant que chaque pile doit être contiguë dans l'espace d'adressage et ne peut pas être déplacée, vous devez réserver une plage d'adresses spécifique à chaque thread. Cela signifie choisir une taille à l'avant. Même si votre espace d'adressage est énorme et que la taille que vous choisissez est vraiment grande, vous devez toujours le choisir dès que vous avez deux threads.
"Aha", dites-vous, "quels sont ces supposés systèmes d'exploitation qui utilisent des piles non contiguës? Je parie que c'est un système universitaire obscur qui ne me sert à rien!". Eh bien, c’est une autre question heureusement déjà posée et à laquelle on a déjà répondu.
la source
Ces structures de données ont généralement des propriétés que la pile de systèmes d'exploitation n'a pas:
Les listes chaînées ne nécessitent pas d'espace d'adressage contigu. Ainsi, ils peuvent ajouter un morceau de mémoire où ils veulent quand ils grandissent.
Même les collections nécessitant un stockage contigu, comme le vecteur C ++, ont un avantage sur les piles de systèmes d'exploitation: elles peuvent déclarer tous les pointeurs / itérateurs non valides à chaque fois qu'ils s'agrandissent. D'autre part, la pile de systèmes d'exploitation doit conserver les pointeurs sur elle jusqu'à ce que la fonction à laquelle appartient la cible appartient.
Un langage de programmation ou un environnement d'exécution peut choisir d'implémenter leurs propres piles non contiguës ou déplaçables pour éviter les limitations des piles de système d'exploitation. Golang utilise de telles piles personnalisées pour prendre en charge un très grand nombre de co-routines, implémentées à l'origine sous forme de mémoire non contiguë et maintenant via des piles déplaçables grâce au suivi de pointeur (voir le commentaire de hobb). Luke et Erlang pourraient également utiliser des piles personnalisées, mais je ne l'ai pas confirmé.
Sur les systèmes 64 bits, vous pouvez configurer des piles relativement grandes à un coût relativement bas, car l'espace d'adressage est abondant et la mémoire physique n'est allouée que lorsque vous l'utilisez réellement.
la source
En pratique, il est difficile (et parfois impossible) de faire fructifier la pile. Comprendre pourquoi nécessite une certaine compréhension de la mémoire virtuelle.
Dans Ye Olde Days d'applications à une seule unité d'exécution et de mémoire contiguë, trois constituaient trois composants d'un espace d'adressage de processus: le code, le segment de mémoire et la pile. La manière dont ces trois éléments étaient disposés dépendait du système d'exploitation, mais le code venait généralement en premier, en commençant par le bas de la mémoire, puis le tas venait grandir, et la pile commençait en haut de la mémoire, puis en bas. Il y avait aussi de la mémoire réservée au système d'exploitation, mais nous pouvons l'ignorer. Les programmes de cette époque présentaient des débordements de pile un peu plus dramatiques: la pile se plantait dans le tas et, en fonction de celui qui était mis à jour en premier, vous utilisiez des données incorrectes ou retourniez d'un sous-programme à une partie arbitraire de la mémoire.
La gestion de la mémoire a quelque peu modifié ce modèle: du point de vue du programme, vous disposiez toujours des trois composants d’une mappe de mémoire de processus, généralement organisés de la même manière, mais chacun des composants était désormais géré en tant que segment indépendant et le MMU signalait la OS si le programme a essayé d'accéder à la mémoire en dehors d'un segment. Une fois que vous avez eu la mémoire virtuelle, il n'était ni nécessaire ni désir de donner à un programme l'accès à tout son espace d'adressage. Donc, les segments ont été assignés des limites fixes.
À ce stade, en supposant un espace d'adressage virtuel suffisant, vous pouvez étendre ces segments si nécessaire et le segment de données (segment de mémoire) s'agrandit au fil du temps: vous démarrez avec un petit segment de données et lorsque l'allocateur de mémoire demande plus d'espace lorsque c'est nécessaire. À ce stade, avec une seule pile, il aurait été physiquement possible d'étendre le segment de pile: le système d'exploitation pourrait empêcher la tentative de pousser quelque chose en dehors du segment et ajouter plus de mémoire. Mais ce n'est pas particulièrement souhaitable non plus.
Entrez multi-threading. Dans ce cas, chaque thread a un segment de pile indépendant, toujours de taille fixe. Mais maintenant, les segments sont disposés les uns après les autres dans l'espace d'adressage virtuel. Il est donc impossible de développer un segment sans en déplacer un autre, ce que vous ne pouvez pas faire car le programme comportera potentiellement des pointeurs sur la mémoire. Vous pouvez également laisser un espace entre les segments, mais cet espace serait gaspillé dans presque tous les cas. Une meilleure approche consistait à imposer un fardeau au développeur d’application: si vous aviez vraiment besoin de piles profondes, vous pouvez le spécifier lors de la création du thread.
Aujourd'hui, avec un espace d'adressage virtuel 64 bits, nous pourrions créer efficacement des piles infinies pour un nombre infini de threads. Mais là encore, cela n’est pas particulièrement souhaitable: dans presque tous les cas, un dépassement de pile indique un bogue avec votre code. Vous fournir une pile de 1 Go retarde simplement la découverte de ce bogue.
la source
La pile ayant une taille maximale fixe n'est pas omniprésente.
Il est également difficile d’obtenir des résultats corrects: les profondeurs des piles suivent une distribution de loi de puissance, ce qui signifie que, quelle que soit la taille de la pile, il restera une fraction importante de fonctions avec des piles encore plus petites (vous perdez donc de l’espace), et quelle que soit la taille de votre travail, il restera des fonctions avec des piles encore plus grandes (vous forcez donc une erreur de débordement de pile pour les fonctions sans erreur). En d'autres termes: quelle que soit la taille choisie, elle sera toujours à la fois trop petite et trop grande.
Vous pouvez résoudre le premier problème en autorisant les piles à démarrer petit et à croître de manière dynamique, mais vous avez toujours le deuxième problème. Et si vous permettez quand même que la pile se développe de façon dynamique, pourquoi alors lui imposer une limite arbitraire?
Il existe des systèmes dans lesquels les piles peuvent se développer de manière dynamique et n'ont pas de taille maximale: Erlang, Go, Smalltalk et Scheme, par exemple. Il y a beaucoup de façons d'implémenter quelque chose comme ça:
Dès que vous disposez de puissantes structures de contrôle-flux non locales, l'idée d'une seule pile contiguë disparaît quand même: les exceptions et les suites résumables, par exemple, "jetteront" la pile, de sorte que vous vous retrouverez avec un réseau de piles (par exemple, implémenté avec une pile de spaghettis). En outre, les systèmes dotés de piles modifiables de première classe, telles que Smalltalk, nécessitent quasiment des piles de spaghettis ou similaires.
la source
Le système d'exploitation doit donner un bloc contigu quand une pile est demandée. La seule façon de le faire est si une taille maximale est spécifiée.
Par exemple, supposons que la mémoire ressemble à ceci pendant la requête (Xs représentent utilisés, Os non utilisés):
XOOOXOOXOOOOOX
Si une demande pour une taille de pile de 6 est demandée, le système d’exploitation répondra non, même si plus de 6 sont disponibles. Si une demande de pile de taille 3 est demandée, la réponse du système d'exploitation sera l'une des zones de 3 emplacements vides (Os) d'affilée.
En outre, on peut voir la difficulté de permettre la croissance lorsque le prochain emplacement contigu est occupé.
Les autres objets mentionnés (Listes, etc.) ne vont pas sur la pile, ils se retrouvent sur le tas dans des zones non contiguës ou fragmentées. Ainsi, lorsqu'ils grandissent, ils ne font que saisir de l'espace, ils n'ont pas besoin d'être contigus. géré différemment.
La plupart des systèmes définissent une valeur raisonnable pour la taille de la pile. Vous pouvez la remplacer lorsque le thread est construit si une taille supérieure est requise.
la source
Sur linux, il s'agit simplement d'une limite de ressource qui existe pour tuer les processus en fuite avant qu'ils ne consomment des quantités nuisibles de la ressource. Sur mon système Debian, le code suivant
produit la sortie
Notez que la limite stricte est définie sur
RLIM_INFINITY
: Le processus est autorisé à augmenter sa limite souple à n’importe quel montant. Cependant, tant que le programmeur n'a aucune raison de penser que le programme a réellement besoin de quantités inhabituelles de mémoire de pile, le processus sera interrompu s'il dépasse une taille de pile de huit mégo-octets.En raison de cette limite, un processus d'emballement (récursion infinie involontaire) est tué longtemps avant de commencer à consommer une quantité de mémoire si importante que le système est obligé de commencer à permuter. Cela peut faire la différence entre un processus en panne et un serveur en panne. Cependant, cela ne limite pas les programmes ayant un besoin légitime pour une pile importante, il leur suffit de définir la limite souple à une valeur appropriée.
Techniquement, les piles croissent de manière dynamique: lorsque la limite souple est définie sur huit mégo-octets, cela ne signifie pas que cette quantité de mémoire a déjà été mappée. Ce serait plutôt inutile, car la plupart des programmes ne se rapprochent jamais de leurs limites respectives. Au lieu de cela, le noyau détectera les accès en dessous de la pile et mappera simplement dans les pages de mémoire selon les besoins. Ainsi, la seule limitation réelle de la taille de la pile est la mémoire disponible sur les systèmes 64 bits (la fragmentation de l'espace d'adressage est plutôt théorique avec une taille d'espace de 16 zébioctets).
la source
La taille maximale de la pile est statique car il s'agit de la définition de "maximum" . Toute sorte de maximum sur n'importe quoi est un chiffre limitatif fixe et convenu. Si elle se comporte comme une cible en mouvement spontané, ce n'est pas un maximum.
Les piles sur les systèmes d'exploitation à mémoire virtuelle se développent en fait de manière dynamique, au maximum .
En parlant de ça, il n'est pas nécessaire que ce soit statique. Au contraire, il peut être configurable, par processus ou par thread, même.
Si la question est "pourquoi y a- t - il une taille de pile maximale" (une taille imposée artificiellement, généralement beaucoup moins que la mémoire disponible)?
Une des raisons est que la plupart des algorithmes ne nécessitent pas une quantité énorme d’espace de pile. Une grande pile est une indication d'une éventuelle récursion . Il est bon d’arrêter la récursivité sans allouer toute la mémoire disponible. L’utilisation dégénérée de la pile, qui peut être provoquée par un scénario de test inattendu, est un problème qui ressemble à une récursion emballée. Par exemple, supposons qu'un analyseur syntaxique pour un opérateur infixe binaire fonctionne en récursant sur l'opérande droit: analyse du premier opérande, opérateur d'analyse, analyse du reste de l'expression. Cela signifie que la profondeur de la pile est proportionnelle à la longueur de l'expression:
a op b op c op d ...
. Un cas de test énorme de cette forme nécessitera une pile énorme. Abandonner le programme quand il atteindra une limite de pile raisonnable entraînera ce problème.Une autre raison de la taille de pile maximale fixe est que l’espace virtuel de cette pile peut être réservé via un type de mappage spécial, et donc garanti. Garanti signifie que l’espace ne sera pas attribué à une autre allocation à laquelle la pile entrera en collision avec elle avant d’atteindre la limite. Le paramètre taille maximale de la pile est requis pour demander ce mappage.
Les threads ont besoin d'une taille de pile maximale pour une raison similaire à celle-ci. Leurs piles sont créées dynamiquement et ne peuvent pas être déplacées si elles entrent en collision avec quelque chose; l'espace virtuel doit être réservé à l'avance et une taille est requise pour cette allocation.
la source