La plupart des architectures que j'ai vues s'appuient sur une pile d'appels pour enregistrer/restaurer le contexte avant les appels de fonction. C'est un paradigme si commun que les opérations Push et pop sont intégrées à la plupart des processeurs. Existe-t-il des systèmes qui fonctionnent sans pile? Si oui, comment fonctionnent-ils et à quoi servent-ils?
Une alternative (quelque peu) populaire à une pile d'appels est suites .
Parrot VM est basé sur la continuation, par exemple. Il est complètement sans pile: les données sont conservées dans des registres (comme Dalvik ou le LuaVM, Parrot est basé sur des registres), et le flux de contrôle est représenté avec des continuations (contrairement à Dalvik ou au LuaVM, qui ont une pile d'appels).
Une autre structure de données populaire, généralement utilisée par les machines virtuelles Smalltalk et LISP, est la pile de spaghetti, qui est un peu comme un réseau de piles.
Comme @ rwong l'a souligné , le style de passage de continuation est une alternative à une pile d'appels. Les programmes écrits en (ou transformés en) style passage-continu ne reviennent jamais, il n'y a donc pas besoin de pile.
Répondre à votre question sous un angle différent: il est possible d'avoir une pile d'appels sans avoir de pile séparée, en allouant les trames de pile sur le tas. Certaines implémentations LISP et Scheme le font.
Autrefois, les processeurs n'avaient pas d'instructions de pile et les langages de programmation ne prenaient pas en charge la récursivité. Au fil du temps, de plus en plus de langues choisissent de prendre en charge la récursivité et une suite matérielle avec des capacités d'allocation de trames de pile. Ce support a beaucoup varié au fil des ans avec différents processeurs. Certains processeurs ont adopté des registres de trame de pile et/ou de pointeur de pile; certaines ont adopté des instructions qui permettraient d'allouer des trames de pile en une seule instruction.
Alors que les processeurs évoluaient avec des caches à niveau unique, puis à plusieurs niveaux, un avantage critique de la pile est celui de la localisation du cache. Le haut de la pile est presque toujours dans le cache. Chaque fois que vous pouvez faire quelque chose qui a un taux d'accès au cache important, vous êtes sur la bonne voie avec les processeurs modernes. Le cache appliqué à la pile signifie que les variables locales, les paramètres, etc. sont presque toujours dans le cache et bénéficient du plus haut niveau de performances.
En bref, l'utilisation de la pile a évolué à la fois au niveau matériel et logiciel. Il existe d'autres modèles (par exemple, le calcul du flux de données a été essayé pendant une période prolongée), cependant, la localisation de la pile le fait très bien fonctionner. De plus, le code procédural est exactement ce que les processeurs veulent, pour les performances: une instruction lui indiquant quoi faire après l'autre. Lorsque les instructions ne sont pas dans l'ordre linéaire, le processeur ralentit énormément, du moins jusqu'à présent, car nous n'avons pas trouvé comment rendre l'accès aléatoire aussi rapide que l'accès séquentiel. (Btw, il y a des problèmes similaires à chaque niveau de mémoire, du cache, à la mémoire principale, au disque ...)
Entre les performances démontrées des instructions d'accès séquentiel et le comportement de mise en cache bénéfique de la pile d'appels, nous avons, au moins actuellement, un modèle de performance gagnant.
(Nous pourrions également lancer la mutabilité des structures de données dans les travaux ...)
Cela ne signifie pas que d'autres modèles de programmation ne peuvent pas fonctionner, surtout lorsqu'ils peuvent être traduits en instructions séquentielles et en modèle de pile d'appels du matériel d'aujourd'hui. Mais il y a un avantage distinct pour les modèles qui prennent en charge l'emplacement du matériel. Cependant, les choses ne restent pas toujours les mêmes, nous pourrions donc voir des changements dans le futur car différentes technologies de mémoire et de transistor permettent plus de parallélisme. C'est toujours une plaisanterie entre les langages de programmation et les capacités matérielles, alors, nous verrons!
TL; DR
Le reste de cette réponse est une collection aléatoire de pensées et d'anecdotes, et donc quelque peu désorganisé.
La pile que vous avez décrite (en tant que mécanisme d'appel de fonction) est spécifique à la programmation impérative.
Sous la programmation impérative, vous trouverez le code machine. Le code machine peut émuler la pile d'appels en exécutant une petite séquence d'instructions.
Sous le code machine, vous trouverez le matériel responsable de l'exécution du logiciel. Alors que le microprocesseur moderne est trop complexe pour être décrit ici, on peut imaginer qu'il existe une conception très simple qui est lente mais qui est toujours capable d'exécuter le même code machine. Une conception aussi simple utilisera les éléments de base de la logique numérique:
Les discussions suivantes contiennent de nombreux exemples de modes alternatifs de structuration des programmes impératifs.
La structure d'un tel programme ressemblera à ceci:
void main(void)
{
do
{
// validate inputs for task 1
// execute task 1, inlined,
// must complete in a deterministically short amount of time
// and limited to a statically allocated amount of memory
// ...
// validate inputs for task 2
// execute task 2, inlined
// ...
// validate inputs for task N
// execute task N, inlined
}
while (true);
// if this line is reached, tell the programmers to prepare
// themselves to appear before an accident investigation board.
return 0;
}
Ce style conviendrait aux microcontrôleurs, c'est-à-dire à ceux qui voient le logiciel comme un compagnon des fonctions du matériel.
Non pas forcément.
Lisez l'ancien document d'Appel La collecte des ordures peut être plus rapide que l'allocation de pile . Il utilise style de passage de continuation et montre une implémentation sans pile.
Notez également que les anciennes architectures informatiques (par exemple IBM/36 ) n'avaient pas de registre de pile matérielle. Mais le système d'exploitation et le compilateur ont réservé un registre pour le pointeur de pile par convention (liée à conventions d'appel ) afin qu'ils puissent avoir un logiciel pile des appels .
En principe, un compilateur et un optimiseur de programme C entier pourraient détecter le cas (quelque peu courant pour les systèmes embarqués) où le graphe d'appel est connu statiquement et sans aucune récursivité (ou pointeurs de fonction). Dans un tel système, chaque fonction pouvait conserver son adresse de retour dans un emplacement statique fixe (et c'était ainsi que Fortran77 fonctionnait dans les ordinateurs de l'ère 1970).
De nos jours, les processeurs ont également des piles d'appels (et des instructions machine d'appel et de retour) conscientes de caches CP .
Vous avez jusqu'à présent de bonnes réponses; permettez-moi de vous donner un exemple peu pratique mais très éducatif de la façon dont vous pourriez concevoir une langue sans la notion de piles ou de "flux de contrôle". Voici un programme qui détermine les factoriels:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = f(3)
Nous mettons ce programme dans une chaîne, et nous évaluons le programme par substitution textuelle. Donc, quand nous évaluons f(3)
, nous faisons une recherche et remplaçons par 3 pour i, comme ceci:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = if 3 == 0 then 1 else 3 * f(3 - 1)
Génial. Maintenant, nous effectuons une autre substitution textuelle: nous voyons que la condition du "si" est fausse et remplaçons une autre chaîne, produisant le programme:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(3 - 1)
Maintenant, nous faisons un autre remplacement de chaîne sur toutes les sous-expressions impliquant des constantes:
function f(i) => if i == 0 then 1 else i * f(i - 1)
let x = 3 * f(2)
Et vous voyez comment cela se passe; Je ne travaillerai pas plus loin. Nous pourrions continuer à faire une série de substitutions de chaînes jusqu'à ce que nous soyons descendus à let x = 6
Et que nous aurions terminé.
Nous utilisons traditionnellement la pile pour les variables locales et les informations de continuation; rappelez-vous, une pile ne vous dit pas d'où vous venez, elle vous indique où vous allez ensuite avec cette valeur de retour en main.
Dans le modèle de substitution de chaînes de programmation, il n'y a pas de "variables locales" sur la pile; les paramètres formels sont substitués à leurs valeurs lorsque la fonction est appliquée à son argument, plutôt que placés dans une table de recherche sur la pile. Et il n'y a pas "d'aller quelque part ensuite" parce que l'évaluation de programme applique simplement des règles simples pour la substitution de chaînes pour produire un programme différent mais équivalent.
Maintenant, bien sûr, faire des substitutions de chaînes n'est probablement pas la voie à suivre. Mais les langages de programmation qui prennent en charge le "raisonnement équationnel" (comme Haskell) utilisent logiquement en utilisant cette technique.
Depuis la publication par Parnas en 1972 de Sur les critères à utiliser pour décomposer les systèmes en modules il a été raisonnablement accepté que les informations se cachant dans les logiciels sont une bonne chose. Cela fait suite à un long débat tout au long des années 60 sur la décomposition structurelle et la programmation modulaire.
Un résultat nécessaire des relations de boîte noire entre les modules mis en œuvre par différents groupes dans tout système multi-thread nécessite un mécanisme pour permettre la réentrance et un moyen de suivre le graphe d'appel dynamique du système. Un flux d'exécution contrôlé doit passer à la fois dans et hors de plusieurs modules.
Dès que l'étendue lexicale est insuffisante pour suivre le comportement dynamique, une comptabilité d'exécution est nécessaire pour suivre la différence.
Étant donné que n'importe quel thread (par définition) n'a qu'un seul pointeur d'instruction en cours, une pile LIFO est appropriée pour suivre chaque invocation.
Ainsi, bien que le modèle de continuation ne maintienne pas explicitement une structure de données pour la pile, il y a toujours l'appel imbriqué de modules qui doit être maintenu quelque part!
Même les langages déclaratifs conservent l'historique de l'évaluation, ou inversement aplatissent le plan d'exécution pour des raisons de performances et maintiennent la progression d'une autre manière.
La structure de boucle sans fin identifiée par rwong est courante dans les applications à haute fiabilité avec une planification statique qui interdit de nombreuses structures de programmation communes mais exige que l'application entière soit considérée comme une boîte blanche sans aucune dissimulation d'informations importantes.
Plusieurs boucles sans fin simultanées ne nécessitent aucune structure pour contenir les adresses de retour car elles n'appellent pas de fonctions, ce qui rend la question théorique. S'ils communiquent à l'aide de variables partagées, celles-ci peuvent facilement dégénérer en analogues d'anciennes adresses de retour de style Fortran.
Tous les anciens mainframes (IBM System/360) n'avaient aucune notion de pile. Sur le 260, par exemple, les paramètres ont été construits dans un emplacement fixe en mémoire et lorsqu'un sous-programme a été appelé, il a été appelé avec R1
pointant vers le bloc de paramètres et R14
contenant l'adresse de retour. La routine appelée, si elle voulait appeler un autre sous-programme, devrait stocker R14
dans un endroit connu avant de passer cet appel.
C'est beaucoup plus fiable qu'une pile, car tout peut être stocké dans des emplacements de mémoire fixes établis au moment de la compilation et il est garanti à 100% que les processus ne manqueront jamais de pile. Il n'y a rien de "Allouer 1 Mo et croiser les doigts" que nous devons faire de nos jours.
Les appels de sous-programmes récursifs ont été autorisés dans PL/I en spécifiant le mot clé RECURSIVE
. Ils signifiaient que la mémoire utilisée par le sous-programme était allouée dynamiquement plutôt que statiquement. Mais les appels récursifs étaient aussi rares à l'époque qu'ils le sont aujourd'hui.
Le fonctionnement sans pile facilite également le multithread massif, c'est pourquoi des tentatives sont souvent faites pour rendre les langages modernes sans pédoncule. Il n'y a aucune raison, par exemple, pourquoi un compilateur C++ ne peut pas être modifié en arrière-plan pour utiliser de la mémoire allouée dynamiquement plutôt que des piles.