Nous connaissons tous et aimons que les appels de fonction sont généralement mis en œuvre à l'aide de la pile; Il y a des cadres, des adresses de retour, des paramètres, du tout.
Cependant, la pile est un détail de mise en œuvre: les conventions d'appel peuvent faire différentes choses (c'est-à-dire que X86 FastCall utilise (certains) des registres, des mips et des suiveurs utilisent des fenêtres de registre, etc.) et des optimisations peuvent faire d'autres choses (d'inlinage, omission de pointeur de cadre, Optimisation des appels de queue ..).
Bien sûr, la présence d'instructions de pile pratiques sur de nombreuses machines (VMS comme JVM et la CLR, mais également de vraies machines telles que X86 avec leur poussée/pop, etc.) Faites-la pratique de l'utiliser pour les appels de fonction, mais dans certains cas, il est possible. Programmer d'une manière dans laquelle une pile d'appels n'est pas nécessaire (je pense à la poursuite du style de passage ici ou d'acteurs d'un système de passage de message)
Donc, je commençais à me demander: est-il possible de mettre en œuvre une sémantique d'appel de fonction sans pile, ni mieux, en utilisant une structure de données différente (une file d'attente, peut-être ou une carte associative?)
[.____] Bien sûr, je comprends qu'une pile est très pratique (il y a une raison pour laquelle il est omniprésent) mais j'ai récemment heurté une mise en œuvre qui m'a fait me demander ..
Est-ce que vous sachiez-vous si cela a déjà été fait dans une machine de langue/machine/virtuelle, et si oui, quels sont les différences et les lacunes frappantes?
EDIT: Mes sentiments d'intestin sont que différentes approches de sous-informatique peuvent utiliser différentes structures de données. Par exemple, Lambda Calculus n'est pas basé sur la pile (l'idée de la fonction de fonction est capturée par des réductions) mais je cherchais une vraie langue/machine/exemple. C'est pourquoi je demande ...
Selon la langue, il peut ne pas être nécessaire d'utiliser une pile d'appels. Les piles d'appel ne sont nécessaires que dans des langues permettant la récursion ou la récursion mutuelle. Si la langue n'autorise pas la récursion, une seule invocation de toute procédure peut être active à tout moment et les variables locales pour cette procédure peuvent être affectées de manière statique. Ces langues doivent prévoir des modifications de contexte, pour la manipulation d'interruption, mais cela ne nécessite toujours pas de pile.
Reportez-vous à la Fortran IV (et antérieure) et les premières versions de COBOL pour des exemples de langues qui ne nécessitent pas de piles d'appel.
Reportez-vous aux données de contrôle des données 6600 (et des machines de données de contrôle antérieures) pour un exemple d'un supercalculateur précoce très performant qui n'a pas fourni de support matériel direct pour les piles d'appel. Reportez-vous au PDP-8 pour un exemple d'un mini-ordinateur précoce très performant qui n'a pas pris en charge les piles d'appels.
Autant que je sache, les machines de piles B5000 Burroughs étaient les premières machines avec des piles d'appel matériel. Les machines B5000 ont été conçues à partir du sol jusqu'à exécuter Algol, qui nécessitait une récursion. Ils ont également eu l'une des premières architectures basées sur descripteur, qui ont établi des travaux de base pour des architectures de capacités.
Autant que je sache, c'était le PDP-6 (qui a grandi dans le déc-10) selon lequel le matériel de pile d'appels popularisé, lorsque la communauté des pirates hachlères a MIT a pris livraison d'un et a découvert que le Fonctionnement Pushj (Adresse de retour Push et saut) Autorisé la routine d'impression décimale à réduire de 50 instructions à 10.
La fonction de fonction la plus élémentaire appelle la sémantique dans une langue permettant la récursion nécessite des capacités qui correspondent bien à une pile. Si c'est tout ce dont vous avez besoin, une pile de base est une bonne correspondance simple. Si vous avez besoin de plus que cela, votre structure de données doit faire plus.
Le meilleur exemple d'avoir besoin de plus que j'ai rencontré est la "continuation", la possibilité de suspendre un calcul au milieu, de l'enregistrer comme une bulle d'état congelée et de les déclencher plus tard, éventuellement plusieurs fois. Les continuations sont devenues populaires dans le dialecte du programme de LISP, comme moyen de mettre en œuvre, entre autres, des sorties d'erreur. Les continuations exigent la possibilité d'instantanément l'environnement d'exécution actuel et de la reproduire plus tard et une pile est quelque peu gênante pour cela.
Structure et interprétation de programmes d'ordinateurs d'Abelson & Sussman " Entre un détail sur les continuations.
Il n'est pas possible de mettre en œuvre une sémantique d'appel de fonction sans utiliser une sorte de pile. Il est seulement possible de jouer à Word Games (par exemple, utilisez un nom différent pour celui-ci, comme "tampon de retour filo").
Il est possible d'utiliser quelque chose qui ne met pas en œuvre la sémantique des appels de fonction (par exemple, le style de passage, les acteurs), puis la transmission de la fonction de la fonction d'appel de la fonction en haut; Mais cela signifie ajouter une sorte de structure de données à suivre où le contrôle est passé lorsque la fonction renvoie et que la structure de données serait un type d'empilement (ou une pile avec un nom/description différent).
Imaginez que vous avez de nombreuses fonctions qui peuvent tous s'appeler. Au moment de l'exécution, chaque fonction doit savoir où revenir lorsque la fonction se déroule. Si first
appelle second
alors vous avez:
second returns to somewhere in first
Ensuite, si second
appelle third
vous avez:
third returns to somewhere in second
second returns to somewhere in first
Ensuite, si third
appelle fourth
vous avez:
fourth returns to somewhere in third
third returns to somewhere in second
second returns to somewhere in first
Comme chaque fonction est appelée, davantage "Où retourner" les informations doivent être stockées quelque part.
Si une fonction renvoie, son "où retourner" les informations est utilisée et n'est plus nécessaire. Par exemple, si fourth
retourne à quelque part dans third
alors le montant de "où retourner à" les informations deviendrait:
third returns to somewhere in second
second returns to somewhere in first
Essentiellement; "Semantitique d'appel de la fonction" implique que:
Ceci décrit un tampon filo/Lifo ou une pile.
Si vous essayez d'utiliser un type d'arbre, chaque nœud de l'arbre n'aura jamais plus d'un enfant. Remarque: Un nœud avec plusieurs enfants ne peut se produire que si une fonction appelle 2 fonctions ou plus en même temps, ce qui nécessite une sorte de concurrence (par exemple des threads, une fourchette (), etc.) et ce ne serait pas "la sémantique d'appel de fonction". Si chaque noeud de l'arbre n'aura jamais plus d'un enfant; Ensuite, cet arbre ne serait utilisé que comme tampon filo/lifo ou une pile; Et comme il n'est utilisé que comme tampon filo/Lifo ou une pile, il est juste de prétendre que "l'arbre" est une pile (et la seule différence est les jeux de mots et/ou les détails de la mise en œuvre).
Il en va de même pour toute autre structure de données pouvant être éventuellement utilisée pour implémenter la "sémantique des appels de fonction" - il sera utilisé comme une pile (et la seule différence est les jeux de mots et/ou les détails de la mise en œuvre); Sauf si cela enfreint la "Semantitique d'appel de la fonction". Remarque: Je fournirais des exemples pour d'autres structures de données si je pouvais, mais je ne peux penser à aucune autre structure légèrement plausible.
Bien sûr, comment une pile est mise en œuvre est un détail de mise en œuvre. Il pourrait s'agir d'une zone de mémoire (où vous gardez une trace d'un "sommet de pile actuel"), il pourrait s'agir d'une sorte de liste liée (où vous gardez une trace de "Entrée actuelle dans la liste"), ou cela pourrait être mis en œuvre dans certains Autre façon. Peu importe également si le matériel ait intégré ou non.
Remarque: Si une seule invocation de toute procédure peut être active à tout moment; Ensuite, vous pouvez affecter statiquement l'espace pour "où retourner à" les informations. Ceci est toujours une pile (par exemple une liste liée d'entrées allouées statiquement allouées utilisées dans une voie filo/LIFEO).
Notez également qu'il y a des choses qui ne suivent pas "la sémantique d'appel de fonction". Ces choses incluent "sémantique potentiellement très différente" (par exemple le passage de la continuation, le modèle d'acteur); et comprend également des extensions communes à la "sémantique des appels de fonction" comme la concurrence (threads, fibres, quoi que ce soit), setjmp
/longjmp
, manipulation des exceptions, etc.
Le langage concaténatif de jouet [~ # ~] xy [~ # ~ ~] utilise une file d'attente d'appel et une pile de données pour l'exécution.
Chaque étape de calcul consistant simplement à désigner le mot suivant à exécuter et dans le cas de cométine, alimentant sa fonction interne, la pile de données et la file d'attente d'appel de données en tant qu'arguments, ou avec Userdefs, poussant les mots le composant à l'avant de la file d'attente.
Donc, si nous avons une fonction pour doubler l'élément supérieur:
; 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
Ensuite, les fonctions de composition +
et dup
ont les signatures de pile/file d'attente suivantes:
// 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]
Et paradoxalement, double
va ressembler à ceci:
double [X Y] -> [X dup^+^Y]
Donc, dans un sens, XY est sans empilement.