Pourquoi les programmes utilisent-ils des piles d'appels si les appels de fonction imbriqués peuvent être en ligne?


33

Pourquoi ne pas demander au compilateur de prendre un programme comme celui-ci:

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

et le convertir en un programme comme celui-ci:

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

éliminant ainsi le besoin de l'ordinateur de se souvenir de l'adresse de retour de c (b)?

Je suppose que l’augmentation de l’espace disque dur et de la mémoire RAM nécessaires pour stocker le programme et prendre en charge sa compilation (respectivement) est la raison pour laquelle nous utilisons des piles d’appels. Est-ce exact?


30
Voyez ce qui se passe si vous faites cela sur un programme de taille significative. En particulier, les fonctions sont appelées de plusieurs endroits.
user253751

10
De plus, parfois, le compilateur ne sait pas quelle fonction est appelée! Exemple idiot:window[prompt("Enter function name","")]()
user253751

26
Comment implémentez-vous function(a)b { if(b>0) return a(b-1); }sans pile?
pjc50

8
Où est la relation avec la programmation fonctionnelle?
mastov

14
@ pjc50: c'est la queue récursive, donc le compilateur la traduit en boucle avec un mutable b. Mais point par point, toutes les fonctions récursives ne peuvent pas éliminer la récursion, et même lorsque la fonction peut en principe, le compilateur peut ne pas être assez intelligent pour le faire.
Steve Jessop

Réponses:


75

Cela s'appelle "inline" et de nombreux compilateurs le font comme stratégie d'optimisation dans les cas où cela a du sens.

Dans votre exemple particulier, cette optimisation permettrait d'économiser de l'espace et du temps d'exécution. Mais si la fonction était appelée à plusieurs endroits du programme (ce qui n'est pas rare!), La taille du code augmenterait, ce qui rendait la stratégie plus incertaine. (Et bien sûr, si une fonction s’appelait directement ou indirectement, il serait impossible de s’inscrire en ligne, car le code deviendrait alors infini en taille.)

Et évidemment, cela n'est possible que pour des fonctions "privées". Les fonctions accessibles aux appelants externes ne peuvent pas être optimisées, du moins pas dans les langues avec liaison dynamique.


7
@Blrfl: Les compilateurs modernes n'ont plus besoin de définitions dans l'en-tête; ils peuvent s'inscrire dans toutes les unités de traduction. Cela nécessite cependant un linker décent. Les définitions dans les fichiers d'en-tête constituent une solution de contournement pour les éditeurs de liens muets.
MSalters

3
"Les fonctions qui sont exposées pour les appelants externes ne peuvent pas être optimisées" - la fonction doit exister, mais n'importe quel site d'appel qui lui est destiné (que ce soit dans votre propre code ou si elles ont la source, celle des appelants externes) peut être en ligne.
Random832

14
Wow, 28 votes positifs pour une réponse qui ne mentionne même pas la raison pour laquelle tout aligner est impossible: Récursion.
mastov

3
@R ..: LTO est l'optimisation du temps LINK, pas l'optimisation du temps LOAD.
MSalters

2
@immibis: Mais si cette pile explicite est introduite par le compilateur, cette pile est la pile d'appels.
user2357112 prend en charge Monica le

51

Votre question comporte deux parties: pourquoi avoir plusieurs fonctions (au lieu de remplacer les appels de fonction par leur définition) et pourquoi implémenter ces fonctions avec des piles d’appel au lieu d’allouer statiquement leurs données ailleurs?

La première raison est la récursivité. Pas seulement le type "oh, faisons un nouvel appel de fonction pour chaque élément de cette liste", mais aussi le type modeste où vous avez peut-être deux appels d'une fonction active en même temps, avec beaucoup d'autres fonctions entre eux. Vous devez mettre des variables locales sur une pile pour supporter cela, et vous ne pouvez pas intégrer de fonctions récursives en ligne en général.

Ensuite, il y a un problème pour les bibliothèques: vous ne savez pas quelles fonctions seront appelées d'où et à quelle fréquence, de sorte qu'une "bibliothèque" ne peut jamais être réellement compilée; elle est uniquement envoyée à tous les clients dans un format de haut niveau pratique inline dans l'application. Mis à part d'autres problèmes, vous perdez complètement la liaison dynamique avec tous ses avantages.

En outre, il existe de nombreuses raisons de ne pas utiliser les fonctions en ligne, même si vous pouviez:

  1. Ce n'est pas nécessairement plus rapide. La configuration et la suppression du cadre de pile représentent peut-être une douzaine d'instructions à cycle unique. Pour de nombreuses fonctions de grande taille ou en boucle, elles ne représentent même pas 0,1% de leur temps d'exécution.
  2. Cela peut être plus lent. La duplication de code a des coûts, par exemple, cela mettra plus de pression dans le cache d'instructions.
  3. Certaines fonctions sont très volumineuses et appelées de nombreux endroits; leur alignement partout augmente les performances binaires bien au delà de ce qui est raisonnable.
  4. Les compilateurs ont souvent de la difficulté avec de très grandes fonctions. Toutes choses étant égales par ailleurs, une fonction de taille 2 * N prend plus de 2 * T temps, alors qu'une fonction de taille N prend T temps.

1
Je suis surpris par le point 4. Quelle est la raison de cela?
JacquesB

12
@JacquesB De nombreux algorithmes d'optimisation sont quadratiques, cubiques ou même techniquement NP-complets. L'exemple canonique est l'allocation de registre, qui est NP-complète par analogie avec la coloration de graphique. (En général, les compilateurs ne tentent pas une solution exacte, mais seulement quelques heuristiques très médiocres s'exécutent en temps linéaire.) De nombreuses optimisations simples en une passe nécessitent des analyses superlinéaires, telles que tout ce qui dépend de la dominance des flux de contrôle (généralement n journal n fois avec n blocs de base).

2
"Vous avez vraiment deux questions ici" Non, je ne le fais pas. Juste un - pourquoi ne pas traiter un appel de fonction comme un espace réservé que le compilateur pourrait, par exemple, remplacer par le code de la fonction appelée?
moonman239

4
@ moonman239 Ensuite, votre libellé m'a jeté. Néanmoins, votre question peut être décomposée comme je le fais dans ma réponse et je pense que c'est une perspective utile.

16

Les piles nous permettent de contourner avec élégance les limites imposées par le nombre fini de registres.

Imaginez avoir exactement 26 globaux "registres az" (ou même n'avoir que les registres de la taille de la puce 8080 de la taille d'un octet) Et chaque fonction que vous écrivez dans cette application partage cette liste à plat.

Un début naïf serait d’attribuer les premiers registres à la première fonction, et sachant que cela ne prenait que 3, commencez par "d" pour la deuxième fonction ... Vous vous épuisez rapidement.

Par contre, si vous avez une bande métaphorique, comme la machine de lecture, vous pouvez faire en sorte que chaque fonction lance un "appel d’une autre fonction" en enregistrant toutes les variables qu’elle utilise et en transmettant la bande (), puis la fonction appelée peut s’embrouiller autant. s'inscrit comme il veut. Lorsque l'appelé est terminé, il renvoie le contrôle à la fonction parent, qui sait où capturer la sortie de l'appelé si nécessaire, puis lit la bande à l'envers pour restaurer son état.

Votre cadre d’appel de base n’est que cela. Il est créé et supprimé par des séquences de code machine normalisées que le compilateur insère autour des transitions d’une fonction à l’autre. (Cela fait longtemps que je ne devais plus me souvenir de mes cadres de pile C, mais vous pouvez lire les différentes manières dont les tâches de qui laisse quoi à X86_calling_conventions .)

(La récursivité est géniale, mais si vous aviez déjà à jongler avec des registres sans pile, vous apprécieriez vraiment les piles.)


Je suppose que l’augmentation de l’espace disque dur et de la mémoire RAM nécessaires pour stocker le programme et prendre en charge sa compilation (respectivement) est la raison pour laquelle nous utilisons des piles d’appels. Est-ce exact?

Bien que nous puissions davantage nous aligner ces temps-ci, ("plus de vitesse" est toujours bon; "moins de ko d'assemblage" signifie très peu dans un monde de flux vidéo). La principale limitation réside dans la capacité du compilateur à aplatir certains types de modèles de code.

Par exemple, les objets polymorphes - si vous ne connaissez pas le seul et unique type d'objet qui vous sera remis, vous ne pouvez pas l'aplatir; vous devez regarder la table des fonctionnalités de l'objet et appeler via ce pointeur ... trivial à faire au moment de l'exécution, impossible à aligner au moment de la compilation.

Une chaîne d’outils moderne peut heureusement intégrer une fonction définie par un polymorphisme quand elle a suffisamment écrasé le ou les appelants pour savoir exactement quelle est la saveur de 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);
}

Dans ce qui précède, le compilateur peut choisir de garder le droit d'inline alignée, d'InlineMe à tout ce qui se trouve à l'intérieur d'act (), sans avoir besoin de toucher les vtables au moment de l'exécution.

Mais toute incertitude quant à la nature de l'objet le laissera comme un appel à une fonction discrète, même si d'autres invocations de la même fonction sont en ligne .


11

Cas que cette approche ne peut pas gérer:

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); };}

Il existe des langues et des plates-formes avec peu ou pas de piles d'appels. Les microprocesseurs PIC ont une pile matérielle limitée à 2 à 32 entrées . Cela crée des contraintes de conception.

COBOL interdit la récursivité: https://stackoverflow.com/questions/27806812/in-cobol-is-it-possible-trecursively- call-a- alinéa

Imposer une interdiction de récursion signifie que vous pouvez représenter le graphe d'appel entier du programme de manière statique en tant que DAG. Votre compilateur pourrait alors émettre une copie d'une fonction pour chaque lieu à partir duquel elle est appelée avec un saut fixe au lieu d'un retour. Aucune pile requise, juste plus d'espace de programme, potentiellement beaucoup pour les systèmes complexes. Mais pour les petits systèmes embarqués, cela signifie que vous pouvez vous assurer de ne pas avoir de débordement de pile au moment de l'exécution, ce qui serait une mauvaise nouvelle pour la commande de papillon de votre réacteur nucléaire / turbine à réaction / voiture, etc.


12
Votre premier exemple est la récursivité de base, et vous avez raison. Mais votre deuxième exemple semble être une boucle for appelant une autre fonction. Intégrer la fonction est différent du déroulement d'une boucle; la fonction peut être alignée sans dérouler la boucle. Ou ai-je oublié un détail subtil?
jpmc26

1
Si votre premier exemple vise à définir la série de Fibonacci, il est faux. (Il manque un fibappel.)
Paŭlo Ebermann

1
Si interdire la récursivité signifie que tout le graphe d'appels peut être représenté sous la forme d'un DAG, cela ne signifie pas que vous pouvez lister la liste complète des séquences d'appels imbriquées dans un espace raisonnable. Sur l'un de mes projets de microcontrôleur avec 128 Ko d'espace de code, j'ai commis l'erreur de demander un graphe d'appels contenant toutes les fonctions pouvant affecter les exigences maximales en RAM de paramètres et ce graphe d'appels dépassant un gig. Un graphe d'appel complet aurait été encore plus long. Il s'agissait d'un programme pouvant contenir 128 Ko d'espace de code.
Supercat

8

Vous voulez une fonction en ligne , et la plupart des compilateurs (en optimisation ) le font.

Notez que l’inline exige que la fonction appelée soit connue (et n’est efficace que si la fonction appelée n’est pas trop grande), puisqu’elle remplace l’appel par la réécriture de la fonction appelée. Ainsi, vous ne pouvez généralement pas insérer une fonction inconnue en ligne (par exemple, un pointeur de fonction contenant des fonctions de bibliothèques partagées liées dynamiquement , ce qui est peut-être visible en tant que méthode virtuelle dans certaines tables , mais certains compilateurs peuvent parfois optimiser via des techniques de dévirtualisation ). Bien sûr, il n’est pas toujours possible d’inclure des fonctions récursives (certains compilateurs astucieux peuvent utiliser une évaluation partielle et, dans certains cas, être capables d’inclure des fonctions récursives).

Notez également que l’insertion de ligne, même si cela est facilement possible, n’est pas toujours efficace: vous (en fait, votre compilateur) pourriez augmenter tellement la taille du code que les caches de processeur (ou prédicteur de branche ) fonctionneraient moins efficacement, ce qui rendrait votre programme exécuté Ralentissez.

Je me concentre un peu sur le style de programmation fonctionnelle , puisque vous avez étiqueté votre question en tant que telle.

Notez qu'il n'est pas nécessaire d'avoir une pile d'appels (du moins dans le sens machine de l'expression "pile d'appels"). Vous ne pouvez utiliser que le tas.

Alors jetez un coup d'œil aux continuations et lisez-en davantage sur le style de passage de continuation (CPS) et sa transformation (intuitivement, vous pouvez utiliser les fermetures de continuation comme des "cadres d'appel" réifiés alloués dans le tas, et ils ressemblent en quelque sorte à une pile d'appels; alors vous avez besoin d'un ramasse-miettes efficace ).

Andrew Appel a écrit un livre Compiler avec des suites et une collecte de vieux papiers peut être plus rapide que l'allocation de pile . Voir aussi l'article de A.Kennedy (ICFP2007) Compiler avec des suites , suite

Je recommande également de lire le livre Lisp In Small Pieces de Queinnec , qui contient plusieurs chapitres sur la suite et la compilation.

Notez également que certaines langues (par exemple, Brainfuck ) ou des machines abstraites (par exemple, OISC , RAM ) ne disposent d’aucune possibilité d’appel, mais sont toujours complètes en turing. Vous n’avez donc (théoriquement) pas besoin d’un mécanisme d’appel de fonction, même si c'est extrêmement pratique. Par ailleurs, certaines anciennes architectures de jeux d’instructions (par exemple, IBM / 370 ) n’ont même pas de pile d’appels matériels, ni d’instructions de machine appelantes (l’IBM / 370 ne dispose que d’une instruction machine Branch and Link )

Enfin, si votre programme complet (y compris toutes les bibliothèques nécessaires) ne comporte aucune récursivité, vous pouvez stocker l'adresse de retour (et les variables "locales", qui deviennent réellement statiques) de chaque fonction dans des emplacements statiques. Les anciens compilateurs Fortran77 l’ont fait au début des années 1980 (les programmes compilés n’utilisaient donc aucune pile d’appels à ce moment-là).


2
C'est très discutable si CPS n'a pas de "pile d'appels". Ce n'est pas sur la pile , la région mystique de la RAM ordinaire qui a un peu de support matériel %esp, etc., mais elle conserve la comptabilité équivalente sur une pile de spaghettis bien nommée dans une autre région de la RAM. L'adresse de retour, en particulier, est essentiellement codée dans la suite. Et bien sûr, les procédures ne sont pas plus rapides (et il me semble que c’est l’objet auquel OP voulait en venir) que de ne passer aucun appel via inline.

Les vieux papiers d'Appel affirmaient (et démontraient avec des analyses comparatives) que CPS pouvait être aussi rapide que d'avoir une pile d'appels.
Basile Starynkevitch le

Je suis sceptique à ce sujet mais peu importe ce n'est pas ce que j'ai affirmé.

1
En fait, il s’agissait du poste de travail MIPS de la fin des années 1980. Probablement, la hiérarchie du cache sur les PC actuels rendrait les performances légèrement différentes. Plusieurs documents ont analysé les revendications d'Appel (et en fait, sur les machines actuelles, l'allocation de pile pourrait être légèrement plus rapide - de quelques pourcents- que la collecte des ordures soigneusement
préparée

1
@Gilles: De nombreux nouveaux cœurs ARM, tels que les Cortex M0 et M3 (et probablement d'autres, comme le M4), prennent en charge la pile matérielle pour des tâches telles que la gestion des interruptions. En outre, le jeu d’instructions Thumb comprend un sous-ensemble limité d’instructions STRM / STRM, y compris STRMDB R13 avec toute combinaison de R0-R7 avec / sans LR et LDRMIA R13 de toute combinaison de R0-R7 avec / sans PC, qui traite efficacement R13 comme un pointeur de pile.
supercat

8

L'inclusion (le remplacement d'appels de fonctions par des fonctionnalités équivalentes) fonctionne bien comme stratégie d'optimisation pour de petites fonctions simples. Les frais généraux liés à un appel de fonction peuvent être échangés contre une petite pénalité liée à la taille du programme (ou, dans certains cas, aucune pénalité).

Cependant, de grandes fonctions appelant d'autres fonctions pourraient entraîner une énorme explosion de la taille du programme si tout était en ligne.

Le but des fonctions appelables est de faciliter une réutilisation efficace, non seulement par le programmeur, mais également par la machine elle-même, et cela inclut des propriétés comme une mémoire raisonnable ou une empreinte sur disque.

Pour ce que ça vaut: vous pouvez avoir des fonctions appelables sans pile d'appels. Par exemple: IBM System / 360. Lors de la programmation dans des langages tels que FORTRAN sur ce matériel, le compteur de programme (adresse de retour) est enregistré dans une petite partie de la mémoire réservée juste avant le point d’entrée de la fonction. Il autorise les fonctions réutilisables, mais pas les codes récursifs ou multithreads (une tentative d'appel récursif ou rentrant entraînerait le remplacement d'une adresse de retour précédemment enregistrée).

Comme expliqué par d'autres réponses, les piles sont de bonnes choses. Ils facilitent la récursion et les appels multithreads. Tout algorithme codé pour utiliser la récursivité peut être codé sans recourir à la récursivité, mais le résultat peut être plus complexe, plus difficile à maintenir et moins efficace. Je ne suis pas sûr qu'une architecture sans pile puisse supporter le multi-threading du tout.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.