web-dev-qa-db-fra.com

Quel est le but d'une pile? Pourquoi en avons-nous besoin?

J'apprends donc maintenant MSIL pour apprendre à déboguer mes applications C # .NET.

Je me suis toujours demandé: quel est l'objectif de la pile?

Juste pour mettre ma question en contexte:
Pourquoi y a-t-il un transfert de mémoire en pile ou "chargement"? D'autre part, pourquoi y a-t-il un transfert de pile en mémoire ou de "stockage"? Pourquoi ne pas simplement les mettre tous en mémoire?

  • Est-ce parce que c'est plus rapide?
  • Est-ce parce que c'est RAM basé?
  • Pour l'efficacité?

J'essaie de comprendre ceci pour m'aider à comprendre CIL code beaucoup plus profondément.

315
Jan Carlo Viray

MISE À JOUR: J'ai tellement aimé cette question que je l'ai faite l'objet de mon blog le 18 novembre 2011 . Merci pour la bonne question!

Je me suis toujours demandé: à quoi sert la pile?

Je suppose que vous parlez de la pile d'évaluation du langage MSIL, et non de la pile réelle par thread au moment de l'exécution.

Pourquoi y a-t-il un transfert de mémoire en pile ou "chargement"? D'autre part, pourquoi y a-t-il un transfert de pile en mémoire ou de "stockage"? Pourquoi ne pas simplement les avoir tous placés dans la mémoire?

MSIL est un langage "machine virtuelle". Des compilateurs comme le compilateur C # génèrent CIL , puis au moment de l'exécution un autre compilateur appelé compilateur JIT (Just In Time) transforme l'IL en code machine réel pouvant être exécuté .

Donc, répondons d'abord à la question "pourquoi avoir MSIL?" Pourquoi ne pas simplement laisser le compilateur C # écrire le code machine?

Parce que c'est moins cher de le faire de cette façon. Supposons que nous ne le fassions pas de cette façon. Supposons que chaque langue doit avoir son propre générateur de code machine. Vous avez vingt langages différents: C #, JScript .NET , Visual Basic, IronPython , F # ... Et supposons que vous ayez dix processeurs différents. Combien de générateurs de code devez-vous écrire? 20 x 10 = 200 générateurs de code. C'est beaucoup de travail. Supposons maintenant que vous souhaitiez ajouter un nouveau processeur. Vous devez écrire le générateur de code vingt fois, un pour chaque langue.

De plus, c'est un travail difficile et dangereux. Écrire des générateurs de code efficaces pour des puces pour lesquelles vous n'êtes pas un expert est un travail difficile! Les concepteurs de compilateurs sont des experts en analyse sémantique de leur langue et non en allocation de registres efficace pour de nouveaux jeux de puces.

Supposons maintenant que nous le fassions comme CIL. Combien de générateurs CIL devez-vous écrire? Un par langue. Combien de compilateurs JIT devez-vous écrire? Un par processeur. Total: 20 + 10 = 30 générateurs de code. De plus, le générateur de langage vers CIL est facile à écrire car CIL est un langage simple, et le générateur de code CIL à machine est également facile à écrire car CIL est un langage simple. Nous nous débarrassons de toutes les subtilités de C # et de VB, etc.

Avoir une langue intermédiaire réduit le coût de production d'un nouveau compilateur de langue de façon spectaculaire. Cela permet également de réduire considérablement le coût de maintenance d'une nouvelle puce. Vous voulez prendre en charge une nouvelle puce, vous trouvez des experts sur cette puce et leur demandez d'écrire une gigue CIL et vous avez terminé; vous supportez alors toutes ces langues sur votre puce.

OK, nous avons donc établi pourquoi nous avons MSIL; car avoir une langue intermédiaire réduit les coûts. Pourquoi alors le langage est-il une "pile machine"?

Parce que les machines empilées sont très simples à gérer pour les rédacteurs de compilateurs de langage Les piles sont un mécanisme simple, facile à comprendre, permettant de décrire des calculs. Sur le plan conceptuel, les machines à pile sont également très faciles à gérer pour les auteurs de compilateur JIT. Utiliser une pile est une abstraction simplificatrice, et donc encore cela réduit nos coûts.

Vous demandez "pourquoi avoir une pile du tout?" Pourquoi ne pas tout faire directement à partir de la mémoire? Eh bien, réfléchissons-y. Supposons que vous souhaitiez générer du code CIL pour:

int x = A() + B() + C() + 10;

Supposons que nous ayons comme convention que "add", "call", "store", etc., prennent toujours leurs arguments de la pile et mettent leur résultat (s'il en existe un) dans la pile. Pour générer du code CIL pour ce C #, il suffit de dire quelque chose comme:

load the address of x // The stack now contains address of x
call A()              // The stack contains address of x and result of A()
call B()              // Address of x, result of A(), result of B()
add                   // Address of x, result of A() + B()
call C()              // Address of x, result of A() + B(), result of C()
add                   // Address of x, result of A() + B() + C()
load 10               // Address of x, result of A() + B() + C(), 10
add                   // Address of x, result of A() + B() + C() + 10
store in address      // The result is now stored in x, and the stack is empty.

Supposons maintenant que nous l'avons fait sans pile. Nous allons le faire à votre façon, où chaque opcode prend les adresses de ses opérandes et l'adresse à laquelle il stocke le résultat:

Allocate temporary store T1 for result of A()
Call A() with the address of T1
Allocate temporary store T2 for result of B()
Call B() with the address of T2
Allocate temporary store T3 for the result of the first addition
Add contents of T1 to T2, then store the result into the address of T3
Allocate temporary store T4 for the result of C()
Call C() with the address of T4
Allocate temporary store T5 for result of the second addition
...

Vous voyez comment ça se passe? Notre code devient énorme car nous devons allouer explicitement tout le stockage temporaire normalement, par convention, aller simplement sur la pile. Pire encore, nos opcodes eux-mêmes deviennent énormes parce qu’ils doivent maintenant prendre pour argument l’adresse dans laquelle ils vont écrire leur résultat, ainsi que l’adresse de chaque opérande. Une instruction "add" qui sait qu'elle va enlever deux choses de la pile et mettre une chose sur peut être un seul octet. Une instruction add qui prend deux adresses d'opérande et une adresse de résultat va être énorme.

Nous utilisons des opcodes basés sur des piles car les piles résolvent le problème commun. À savoir: Je souhaite allouer un stockage temporaire, l'utiliser très bientôt, puis m'en débarrasser rapidement lorsque j'ai terminé . En supposant que nous avons une pile à notre disposition, nous pouvons rendre les opcodes très petits et le code très concis.

MISE À JOUR: Quelques réflexions supplémentaires

Incidemment, cette idée de réduction drastique des coûts en (1) spécifiant une machine virtuelle, (2) en écrivant des compilateurs qui ciblent le langage VM et (3) en écrivant des implémentations de VM sur divers matériels , n’est pas une idée nouvelle du tout. Il n'a pas été créé avec MSIL, LLVM, bytecode Java ni aucune autre infrastructure moderne. La première implémentation de cette stratégie à ma connaissance est la machine pcode de 1966.

La première fois que j'ai personnellement entendu parler de ce concept, c’est quand j’ai appris comment les développeurs d’Infocom avaient réussi à obtenir Zork fonctionnant sur autant de machines différentes. Ils ont spécifié une machine virtuelle appelée Z-machine , puis ont créé des émulateurs Z-machine pour tout le matériel sur lequel ils voulaient exécuter leurs jeux. Cela présentait l'énorme avantage supplémentaire qu'ils pouvaient implémenter gestion de la mémoire virtuelle sur des systèmes primitifs à 8 bits; un jeu peut être plus volumineux que ce qui pourrait être stocké dans la mémoire car ils pourraient simplement rechercher le code à partir du disque lorsqu'ils en ont besoin et le supprimer lorsqu'ils doivent charger un nouveau code.

434
Eric Lippert

Gardez à l'esprit que lorsque vous parlez de MSIL, vous parlez également d'instructions pour une machine virtuelle . La VM utilisée dans .NET est une machine virtuelle basée sur une pile. À la différence d'une VM basée sur des registres, le Dalvik VM utilisé dans les systèmes d'exploitation Android en est un exemple.

La pile dans la VM est virtuelle, il appartient à l'interpréteur ou au compilateur juste à temps de traduire les instructions VM en code réel qui s'exécute sur le processeur. Ce qui, dans le cas de .NET, est presque toujours une gigue, le jeu d'instructions MSIL a été conçu pour être jeté dès le départ. Contrairement à Java bytecode par exemple, il contient des instructions distinctes pour les opérations sur des types de données spécifiques. Ce qui le rend optimisé pour être interprété. Un interpréteur MSIL existe cependant, il est utilisé dans le .NET Micro Framework. Qui fonctionne sur des processeurs avec des ressources très limitées, ne peut pas se permettre le RAM nécessaire pour stocker le code machine.

Le modèle de code machine actuel est mixte, avec une pile et des registres. L’un des gros travaux de l’optimiseur de code JIT consiste à trouver des moyens de stocker des variables conservées dans la pile dans des registres, ce qui améliore considérablement la vitesse d’exécution. Une gigue de Dalvik a le problème opposé.

La pile de machines est par ailleurs une installation de stockage très basique, utilisée depuis très longtemps dans les concepteurs de processeurs. Il possède une très bonne localité de référence, une fonctionnalité très importante sur les processeurs modernes qui traitent les données beaucoup plus rapidement que ne le permet RAM et prend en charge la récursion. La conception du langage est fortement influencée par la présence d'une pile, visible dans le support des variables locales et dont la portée est limitée au corps de la méthode. Un problème important avec la pile est celui pour lequel ce site est nommé.

86
Hans Passant

Il existe un article très intéressant/détaillé sur ce sujet dans Wikipedia Avantages des jeux d'instructions de la pile. J'aurais besoin de le citer entièrement, il est donc plus facile de simplement mettre un lien. Je vais simplement citer les sous-titres

  • Code objet très compact
  • Compilateurs simples/interprètes simples
  • Etat minimal du processeur
20
user468687

Pour ajouter un peu plus à la question de la pile. Le concept de pile découle de la conception de la CPU dans laquelle le code machine de l'unité de logique arithmétique (ALU) fonctionne sur des opérandes situés sur la pile. Par exemple, une opération de multiplication peut prendre les deux opérandes supérieurs de la pile, les multiplier et replacer le résultat sur la pile. Le langage machine a généralement deux fonctions de base pour ajouter et supprimer des opérandes de la pile; Push et POP. Dans de nombreux processeurs de signal numérique (cpu) et contrôleurs de machine (tels que ceux contrôlant une machine à laver), la pile est située sur la puce elle-même. Cela donne un accès plus rapide à l'ALU et consolide les fonctionnalités requises en une seule puce.

8
skyman

Si le concept pile/tas n'est pas suivi et que les données sont chargées dans un emplacement de mémoire aléatoire OR, les données sont stockées à partir d'emplacements de mémoire aléatoires ... ce sera très non structuré et non géré.

Ces concepts permettent de stocker des données dans une structure prédéfinie afin d'améliorer les performances, l'utilisation de la mémoire ... et donc les structures de données.

5
Azodious

On peut avoir un système fonctionnant sans piles, en utilisant style de passage contin du codage. Les cadres d’appel deviennent alors des continuations allouées dans le segment de mémoire collecté (le récupérateur de mémoire a besoin d’une pile).

Voir les anciens écrits d'Andrew Appel: compilation avec des suites et le ramassage des ordures peut être plus rapide que l'allocation de pile

(Il a peut-être un peu tort aujourd'hui à cause de problèmes de cache)

4