web-dev-qa-db-fra.com

Comment sait-on que les variables sont dans des registres ou sur pile?

Je lis cette question sur inline le isocpp FAQ , le code est donné comme

void f()
{
  int x = /*...*/;
  int y = /*...*/;
  int z = /*...*/;
  // ...code that uses x, y and z...
  g(x, y, z);
  // ...more code that uses x, y and z...
 }

alors il dit que

En supposant une implémentation C++ typique qui a des registres et une pile, les registres et les paramètres sont écrits dans la pile juste avant l'appel à g(), puis les paramètres sont lus à partir de la pile à l'intérieur de g() et relisez pour restaurer les registres pendant que g() revient à f(). Mais c'est beaucoup de lecture et d'écriture inutiles, surtout dans les cas où le compilateur est capable d'utiliser des registres pour les variables x, y et z: chaque variable pourrait être écrite deux fois (en tant que registre et également en tant que paramètre) et lu deux fois (lorsqu'il est utilisé dans g() et pour restaurer les registres lors du retour à f()).

J'ai beaucoup de mal à comprendre le paragraphe ci-dessus. J'essaie de lister mes questions comme ci-dessous:

  1. Pour qu'un ordinateur effectue certaines opérations sur certaines données qui résident dans la mémoire principale, est-il vrai que les données doivent d'abord être chargées dans certains registres, puis la CPU peut fonctionner sur les données? (Je sais que cette question n'est pas particulièrement liée au C++, mais sa compréhension sera utile pour comprendre le fonctionnement du C++.)
  2. Je pense que f() est une fonction de la même manière que g(x, y, z) est une fonction. Comment se fait-il que x, y, z Avant d'appeler g() soit dans les registres et que les paramètres passés dans g() soient sur la pile?
  3. Comment sait-on que les déclarations pour x, y, z Les stockent dans les registres? Où les données à l'intérieur de g() sont stockées, enregistrées ou empilées?

[~ # ~] ps [~ # ~]

Il est très difficile de choisir une réponse acceptable lorsque les réponses sont toutes très bonnes (par exemple, celles fournies par @MatsPeterson, @TheodorosChatzigiannakis et @superultranova) je pense. Personnellement, j'aime un peu plus celui de @Potatoswatter, car la réponse offre quelques lignes directrices.

42
Allanqunzi

Ne prenez pas ce paragraphe trop au sérieux. Il semble faire des hypothèses excessives et entrer ensuite dans des détails excessifs, qui ne peuvent pas vraiment être généralisés.

Mais vos questions sont très bonnes.

  1. Pour qu'un ordinateur effectue certaines opérations sur certaines données qui résident dans la mémoire principale, est-il vrai que les données doivent d'abord être chargées dans certains registres, puis la CPU peut fonctionner sur les données? (Je sais que cette question n'est pas particulièrement liée au C++, mais sa compréhension sera utile pour comprendre le fonctionnement du C++.)

Plus ou moins, tout doit être chargé dans des registres. La plupart des ordinateurs sont organisés autour d'un datapath, un bus reliant les registres, les circuits arithmétiques et le niveau supérieur de la hiérarchie mémoire. Habituellement, tout ce qui est diffusé sur le chemin de données est identifié par un registre.

Vous vous souvenez peut-être du grand débat RISC vs CISC. L'un des points clés était que la conception d'un ordinateur peut être beaucoup plus simple si la mémoire n'est pas autorisée à se connecter directement aux circuits arithmétiques.

Dans les ordinateurs modernes, il y a registres architecturaux, qui sont une construction de programmation comme une variable, et registres physiques, qui sont des circuits réels. Le compilateur fait beaucoup de travail pour garder une trace des registres physiques tout en générant un programme en termes de registres architecturaux. Pour un jeu d'instructions CISC comme x86, cela peut impliquer la génération d'instructions qui envoient des opérandes en mémoire directement aux opérations arithmétiques. Mais dans les coulisses, c'est inscrit tout le long.

Conclusion: laissez simplement le compilateur faire son travail.

  1. Je pense que f() est une fonction de la même manière que g (x, y, z) est une fonction. Comment se fait-il que x, y, z avant d'appeler g() sont dans les registres et les paramètres passés dans g() sont sur la pile?

Chaque plate-forme définit un moyen pour les fonctions C de s’appeler. Passer des paramètres dans des registres est plus efficace. Mais il y a des compromis et le nombre total de registres est limité. Les ABI plus anciens sacrifiaient plus souvent l'efficacité pour la simplicité et les mettaient tous sur la pile.

Bottom line: L'exemple suppose arbitrairement un ABI naïf.

  1. Comment sait-on que les déclarations pour x, y, z les stockent dans les registres? Où les données à l'intérieur g() sont stockées, enregistrées ou empilées?

Le compilateur a tendance à préférer utiliser des registres pour les valeurs les plus fréquemment utilisées. Rien dans l'exemple ne nécessite l'utilisation de la pile. Cependant, des valeurs moins fréquemment utilisées seront placées sur la pile pour rendre plus de registres disponibles.

Uniquement lorsque vous prenez l'adresse d'une variable, par exemple par &x ou passant par référence, et cette adresse échappe au inliner, le compilateur doit utiliser de la mémoire et non des registres.

En résumé: évitez de prendre des adresses et de les passer/stocker de bon gré.

39
Potatoswatter

Il appartient entièrement au compilateur (en conjonction avec le type de processeur) si une variable est stockée dans la mémoire ou un registre [ou dans certains cas plus d'un registre] (et quelles options vous donnez au compilateur, en supposant qu'il a des options pour décider de telles choses - la plupart des "bons" compilateurs le font). Par exemple, le compilateur LLVM/Clang utilise une passe d'optimisation spécifique appelée "mem2reg" qui déplace les variables de la mémoire vers les registres. La décision de le faire est basée sur la façon dont la ou les variables sont utilisées - par exemple, si vous prenez l'adresse d'une variable à un moment donné, elle doit être en mémoire.

D'autres compilateurs ont des fonctionnalités similaires, mais pas nécessairement identiques.

De plus, au moins dans les compilateurs qui ont un semblant de portabilité, il y aura AUSSI une phase de génération de code machine pour la cible réelle, qui contient des optimisations spécifiques à la cible, qui peuvent à nouveau déplacer une variable de la mémoire vers un registre.

Il n'est pas possible [sans comprendre comment fonctionne le compilateur particulier] de déterminer si les variables de votre code sont dans des registres ou en mémoire. On peut deviner, mais une telle supposition revient à deviner d'autres "sortes de choses prévisibles", comme regarder par la fenêtre pour deviner s'il va pleuvoir dans quelques heures - selon l'endroit où vous vivez, cela peut être une supposition aléatoire complète , ou tout à fait prévisible - dans certains pays tropicaux, vous pouvez régler votre montre en fonction du moment où la pluie arrive chaque après-midi, dans d'autres pays, il pleut rarement, et dans certains pays, comme ici en Angleterre, vous ne pouvez pas savoir avec certitude au-delà " en ce moment il ne pleut pas ici ".

Pour répondre aux questions réelles:

  1. Cela dépend du processeur. Les processeurs RISC appropriés tels que ARM, MIPS, 29K, etc. n'ont pas d'instructions qui utilisent des opérandes de mémoire, à l'exception des instructions de type chargement et stockage. Donc, si vous devez ajouter deux valeurs, vous devez charger les valeurs dans les registres et utiliser l'opération d'ajout sur ces registres. Certains, tels que x86 et 68K, permettent à l'un des deux opérandes d'être un opérande mémoire, et par exemple PDP-11 et VAX ont la "liberté totale", que vos opérandes soient en mémoire ou en registre, vous pouvez utiliser la même instruction, juste différents modes d'adressage pour les différents opérandes.
  2. Votre prémisse d'origine ici est fausse - il n'est pas garanti que les arguments de g soient sur la pile. Ce n'est là qu'une des nombreuses options. De nombreux ABI (interface binaire d'application, ou "conventions d'appel") utilisent des registres pour les premiers arguments d'une fonction. Donc, encore une fois, cela dépend du compilateur (dans une certaine mesure) et du processeur (beaucoup plus que le compilateur) que le compilateur cible. si les arguments sont en mémoire ou dans des registres.
  3. Encore une fois, c'est une décision prise par le compilateur - cela dépend du nombre de registres du processeur, qui sont disponibles, quel est le coût si "libérer" certains registres pour x, y et z - qui va de "pas de frais du tout" à "pas mal" - là encore, selon le modèle de processeur et l'ABI.
15
Mats Petersson

Pour qu'un ordinateur effectue certaines opérations sur certaines données qui résident dans la mémoire principale, est-il vrai que les données doivent d'abord être chargées dans certains registres, puis la CPU peut fonctionner sur les données?

Même cette affirmation n'est pas toujours vraie. C'est probablement vrai pour toutes les plates-formes avec lesquelles vous travaillerez jamais, mais il peut sûrement y avoir une autre architecture qui n'utilise pas du tout registres du processeur .

Votre ordinateur x86_64 le fait cependant.

Je pense que f() est une fonction de la même manière que g (x, y, z) est une fonction. Comment se fait-il que x, y, z avant d'appeler g() sont dans les registres et les paramètres passés dans g() sont sur la pile?

Comment sait-on que les déclarations pour x, y, z les stockent dans les registres? Où les données à l'intérieur g() sont stockées, enregistrées ou empilées?

Ces deux questions ne peuvent recevoir de réponse unique pour aucun compilateur et système sur lequel votre code sera compilé. Ils ne peuvent même pas être pris pour acquis car les paramètres de g peuvent ne pas être sur la pile, tout dépend de plusieurs concepts que je vais expliquer ci-dessous.

Tout d'abord, vous devez être conscient des soi-disant conventions d'appel qui définissent, entre autres, comment les paramètres de fonction sont passés (par exemple, poussés sur la pile, placés dans des registres ou un mélange des deux). Cela n'est pas imposé par la norme C++ et les conventions d'appel font partie de ABI , un sujet plus large concernant les problèmes de programme de code machine de bas niveau.

Deuxièmement allocation de registre (c'est-à-dire quelles variables sont réellement chargées dans un registre à un moment donné) est une tâche complexe et un problème NP-complete . Les compilateurs essaient de faire de leur mieux avec les informations dont ils disposent. En général, les variables moins fréquemment utilisées sont placées sur la pile tandis que les variables plus fréquemment utilisées sont conservées dans les registres. Ainsi, la partie Where the data inside g() is stored, register or stack? ne peut pas être répondue une fois pour toutes car elle dépend de nombreux facteurs dont registre pression .

Sans parler des optimisations du compilateur qui pourraient même éliminer le besoin de certaines variables d'être autour.

Enfin, la question que vous avez liée indique déjà

Naturellement, votre kilométrage peut varier, et il y a un million de variables qui sortent du cadre de cette FAQ particulière, mais ce qui précède sert d'exemple du genre de choses qui peuvent se produire avec l'intégration procédurale.

c'est-à-dire que le paragraphe que vous avez posté fait quelques hypothèses pour mettre les choses en place pour un exemple. Ce ne sont que des hypothèses et vous devez les traiter comme telles.

Comme un petit ajout: concernant les avantages de inline sur une fonction, je recommande de jeter un œil à cette réponse: https://stackoverflow.com/a/145952/193816

7
Marco A.

Vous ne pouvez pas savoir, sans regarder le langage Assembly, si une variable est dans un registre, une pile, un tas, une mémoire globale ou ailleurs. Un variable est un concept abstrait. Le compilateur est autorisé à utiliser des registres ou une autre mémoire comme il le souhaite, tant que l'exécution n'est pas modifiée.

Il existe également une autre règle qui affecte ce sujet. Si vous prenez l'adresse d'une variable et la stockez dans un pointeur, la variable ne peut pas être placée dans un registre car les registres n'ont pas d'adresse.

Le stockage variable peut également dépendre des paramètres d'optimisation du compilateur. Les variables peuvent disparaître en raison de la simplification. Les variables qui ne changent pas de valeur peuvent être placées dans l'exécutable en tant que constante.

5
Thomas Matthews

Concernant votre question n ° 1, oui, les instructions de non chargement/stockage fonctionnent sur les registres.

Concernant votre question n ° 2, si nous supposons que des paramètres sont passés sur la pile, alors nous devons écrire les registres dans la pile, sinon g() ne pourra pas accéder à la les données, car le code dans g() ne "sait" pas qui enregistre les paramètres.

Concernant votre question n ° 3, on ne sait pas que x, y et z seront à coup sûr stockés dans des registres en f (). On pourrait utiliser le mot clé register, mais c'est plus une suggestion. Sur la base de la convention d'appel, et en supposant que le compilateur n'effectue aucune optimisation impliquant le passage de paramètres, vous pourrez peut-être prédire si les paramètres sont sur la pile ou dans des registres.

Vous devez vous familiariser avec les conventions d'appel. Les conventions d'appel traitent de la façon dont les paramètres sont passés aux fonctions et impliquent généralement le passage des paramètres sur la pile dans un ordre spécifié, en plaçant les paramètres dans des registres ou une combinaison des deux.

stdcall, cdecl et fastcall sont quelques exemples de conventions d'appel. En termes de passage de paramètres, stdcall et cdecl sont les mêmes, dans les paramètres sont poussés dans l'ordre de droite à gauche sur la pile. Dans ce cas, si g() était cdecl ou stdcall l'appelant pousserait z, y, x dans cet ordre:

mov eax, z
Push eax
mov eax, x
Push eax
mov eax, y
Push eax
call g

Dans l'appel rapide 64 bits, des registres sont utilisés, Microsoft utilise RCX, RDX, R8, R9 (plus la pile pour les fonctions nécessitant plus de 4 paramètres), Linux utilise RDI, RSI, RDX, RCX, R8, R9. Pour appeler g() en utilisant MS 64bit fastcall, on procéderait comme suit (nous supposons que z, x et y ne sont pas dans registres)

mov rcx, x
mov rdx, y
mov r8, z
call g

C'est ainsi que Assembly est écrit par les humains, et parfois les compilateurs. Les compilateurs utiliseront quelques astuces pour éviter de passer des paramètres, car cela réduit généralement le nombre d'instructions et peut réduire le nombre d'accès à la mémoire. Prenons l'exemple du code suivant (j'ignore intentionnellement les règles de registre non volatiles):

f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
call g
mov rcx, rax
ret

g:
mov rax, rsi
add rax, rcx
add rax, rdx
ret

À des fins d'illustration, rcx est déjà utilisé et x a été chargé dans rsi. Le compilateur peut compiler g de telle sorte qu'il utilise rsi au lieu de rcx, donc les valeurs n'ont pas à être échangées entre les deux registres quand vient le temps d'appeler g. Le compilateur peut également intégrer g, maintenant que f et g partagent le même ensemble de registres pour x, y et z. Dans ce cas, le call g l'instruction serait remplacée par le contenu de g, à l'exclusion de l'instruction ret.

f:
xor rcx, rcx
mov rsi, x
mov r8, z
mov rdx y
mov rax, rsi
add rax, rcx
add rax, rdx
mov rcx, rax
ret

Ce sera encore plus rapide, car nous n'avons pas à traiter l'instruction call, car g a été inséré dans f.

3
superultranova

Réponse courte: vous ne pouvez pas. Cela dépend complètement de votre compilateur et des fonctionnalités d'optimisation activées.

Le souci du compilateur est de traduire en assembleur votre programme, mais comment cela se fait est étroitement lié au fonctionnement de votre compilateur. Certains compilateurs vous permettent d'indiquer quelle carte variable enregistrer. Vérifiez par exemple ceci: https://gcc.gnu.org/onlinedocs/gcc/Global-Reg-Vars.html

Votre compilateur appliquera des transformations à votre code afin de gagner quelque chose, peut-être des performances, peut-être une taille de code inférieure, et il appliquera des fonctions de coût pour estimer ces gains, de sorte que vous ne pouvez normalement voir que le résultat du démontage de l'unité compilée.

Pour qu'un ordinateur effectue certaines opérations sur certaines données qui résident dans la mémoire principale, est-il vrai que les données doivent d'abord être chargées dans certains registres, puis la CPU peut fonctionner sur les données?

Cela dépend de l'architecture et du jeu d'instructions qu'il propose. Mais dans la pratique, oui - c'est le cas typique.

Comment sait-on que les déclarations pour x, y, z les stockent dans les registres? Où les données à l'intérieur g() sont stockées, enregistrées ou empilées?

En supposant que le compilateur n'élimine pas les variables locales, il préférera les mettre dans des registres, car les registres sont plus rapides que la pile (qui réside dans la mémoire principale ou un cache).

Mais ceci est loin d'être une vérité universelle: cela dépend du fonctionnement interne (compliqué) du compilateur (dont les détails sont passés à la main dans ce paragraphe).

Je pense que f() est une fonction de la même manière que g (x, y, z) est une fonction. Comment se fait-il que x, y, z avant d'appeler g() sont dans les registres et les paramètres passés dans g() sont sur la pile?

Même si nous supposons que les variables sont, en fait, stockées dans les registres, lorsque vous appelez une fonction, la convention d'appel = entre en jeu. C'est une convention qui décrit comment une fonction est appelée, où les arguments sont passés, qui nettoie la pile, quels registres sont conservés.

Toutes les conventions d'appel ont une sorte de surcharge. L'une des sources de ce surcoût est l'argument transmis. De nombreuses conventions d'appel tentent de réduire cela, en préférant passer des arguments via des registres, mais comme le nombre de registres CPU est limité (par rapport à l'espace de la pile), elles finissent par revenir à pousser la pile après un certain nombre d'arguments.

Le paragraphe de votre question suppose une convention d'appel qui passe tout à travers la pile et basée sur cette hypothèse, ce qu'il essaie de vous dire est qu'il serait avantageux (pour la vitesse d'exécution) si nous pouvions "copier" (au moment de la compilation) le corps de la fonction appelée à l'intérieur de l'appelant (au lieu d'émettre un appel à la fonction). Cela donnerait logiquement les mêmes résultats, mais cela éliminerait le coût d'exécution de l'appel de fonction.

Les variables sont presque toujours stockées dans la mémoire principale. Plusieurs fois, en raison des optimisations du compilateur, la valeur de votre variable déclarée ne se déplacera jamais dans la mémoire principale, mais ce sont des variables intermédiaires que vous utilisez dans votre méthode qui ne sont pas pertinentes avant l'appel d'une autre méthode (c'est-à-dire l'occurrence de l'opération de pile).

C'est par conception - pour améliorer les performances car il est plus facile (et beaucoup plus rapide) pour le processeur d'adresser et de manipuler les données dans les registres. Les registres architecturaux sont limités en taille, donc tout ne peut pas être mis dans des registres. Même si vous "faites allusion" à votre compilateur pour le mettre dans le registre, le système d'exploitation peut éventuellement le gérer en dehors du registre, dans la mémoire principale, si les registres disponibles sont pleins.

Très probablement, une variable sera dans la mémoire principale car elle conserve sa pertinence dans l'exécution presque et peut conserver sa fiabilité pendant une plus longue période de temps CPU. Une variable est dans le registre architectural car elle est pertinente dans les prochaines instructions machine et l'exécution sera presque immédiate mais peut ne pas être pertinente pour longtemps.

1
Yogee