42 votes

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

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

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 est dit que

En supposant une implémentation typique de C++ qui a des registres et une pile, les registres et les paramètres sont écrits sur la pile juste avant l'appel appel à g() puis les paramètres sont lus à partir de la pile à l'intérieur de l'appareil g() et relire pour restaurer les registres pendant que g() retourne à f() . Mais cela fait beaucoup de lecture et d'écriture inutiles, surtout dans les cas où le compilateur est capable d'utiliser les registres pour les variables x , y et z chaque variable peut être écrite deux fois (comme registre et comme également comme paramètre) et lue deux fois (lorsqu'elle est utilisée à l'intérieur de g() et pour restaurer les registres lors du retour à f() ).

J'ai une grande difficulté à comprendre le paragraphe ci-dessus. J'essaie d'énumérer mes questions comme suit :

  1. Pour qu'un ordinateur puisse effectuer des opérations sur des données qui se trouvent dans la mémoire principale, est-il vrai que les données doivent d'abord être chargées dans des registres, puis l'unité centrale de traitement peut effectuer des opérations sur ces données ? (Je sais que cette question n'est pas particulièrement liée au C++, mais comprendre cela sera utile pour comprendre le fonctionnement du C++).
  2. Je pense 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 en g() sont sur la pile ?
  3. Comment sait-on que les déclarations de x, y, z les faire stocker dans les registres ? Où les données à l'intérieur g() est stocké, registre ou pile ?

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 la réponse de @Potatoswatter, car elle propose des lignes directrices.

39voto

Potatoswatter Points 70305

Ne prenez pas ce paragraphe trop au sérieux. Il semble faire des hypothèses excessives, puis entrer 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 puisse effectuer des opérations sur des données qui se trouvent dans la mémoire principale, est-il vrai que les données doivent d'abord être chargées dans des registres, puis l'unité centrale de traitement peut effectuer des opérations sur ces données ? (Je sais que cette question n'est pas particulièrement liée au C++, mais comprendre cela 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 chemin de données un bus qui relie les registres, les circuits arithmétiques et le niveau supérieur de la hiérarchie de la mémoire. En général, 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 d'architecture qui sont une construction de programmation comme une variable, et registres physiques qui sont des circuits réels. Le compilateur fait beaucoup d'efforts pour garder la trace des registres physiques tout en générant un programme en termes de registres architecturaux. Pour un jeu d'instructions CISC comme le x86, cela peut impliquer de générer des instructions qui envoient des opérandes en mémoire directement vers des opérations arithmétiques. Mais dans les coulisses, ce sont les registres qui sont à l'honneur.

La ligne du bas : Laissez 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() soient dans les registres, et que les paramètres passés dans g() soient sur la pile ?

Chaque plate-forme définit une manière pour les fonctions C de s'appeler entre elles. Parfois, il est indiqué de mettre les paramètres sur la pile. Habituellement, au moins certains paramètres peuvent être passés dans des registres parce que c'est plus efficace.

En résumé : Parce qu'une règle arbitraire le dit.

  1. Comment sait-on que les déclarations pour x, y, z les font stocker dans les registres ? Où sont stockées les données dans g(), registre ou pile ?

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

Ce n'est que lorsque vous prenez l'adresse d'une variable, comme en &x ou le passage par référence, et que l'adresse échappe à l'inliner, est-ce que le compilateur doit utiliser la mémoire et non les registres.

La ligne du bas : Évitez de prendre des adresses et de les transmettre/stocker bon gré mal gré.

15voto

Mats Petersson Points 70074

C'est entièrement au compilateur (en conjonction avec le type de processeur) de décider si une variable est stockée en mémoire ou dans un registre [ou dans certains cas plus d'un registre] (et quelles options vous donnez au compilateur, en supposant qu'il ait des options pour décider de telles choses - la plupart des "bons" compilateurs en ont). 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 encore déplacer une variable de la mémoire vers un registre.

Il n'est pas possible [sans comprendre comment fonctionne le compilateur en question] de déterminer si les variables de votre code sont dans des registres ou en mémoire. On peut deviner, mais une telle supposition est juste comme deviner d'autres "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 complètement aléatoire, 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à de "en ce moment, il [ne] pleut pas ici".

Pour répondre aux questions actuelles :

  1. Cela dépend du processeur. Les processeurs RISC classiques tels que ARM, MIPS, 29K, etc. n'ont pas d'instructions qui utilisent des opérandes mémoire, à l'exception des instructions de type load et store. Ainsi, si vous devez additionner deux valeurs, vous devez charger les valeurs dans des registres et utiliser l'opération d'addition sur ces registres. Certains, comme x86, permettent à l'une des deux opérandes d'être une opérande mémoire, et par exemple 68K, PDP-11 et VAX ont une "liberté totale", que vos opérandes soient en mémoire ou dans un registre, vous pouvez utiliser la même instruction, juste des modes d'adressage différents pour les différentes opérandes.
  2. Votre prémisse initiale est erronée - il n'est pas garanti que les arguments en faveur de g sont sur la pile. Ce n'est qu'une option parmi d'autres. De nombreuses ABI (interface binaire d'application, alias "conventions d'appel") utilisent des registres pour les premiers arguments d'une fonction. Donc, encore une fois, le fait que les arguments soient en mémoire ou dans des registres dépend du compilateur (dans une certaine mesure) et du processeur (bien plus que du compilateur) que le compilateur cible.
  3. Encore une fois, il s'agit d'une décision prise par le compilateur - cela dépend du nombre de registres dont dispose le processeur, de ceux qui sont disponibles, de ce qu'il en coûte de "libérer" un registre pour l'utilisation d'un autre. x , y et z - qui va de "aucun coût" à "un peu" - là encore, en fonction du modèle de processeur et de l'ABI.

7voto

Marco A. Points 18535

Pour qu'un ordinateur puisse effectuer des opérations sur des données qui se trouvent dans la mémoire principale, est-il vrai que les données doivent d'abord être chargées dans des registres, puis l'unité centrale de traitement peut effectuer des opérations sur ces données ?

Même cette affirmation n'est pas toujours vraie. Elle est probablement vraie pour toutes les plates-formes avec lesquelles vous travaillerez un jour, mais il peut sûrement y avoir une autre architecture qui ne fait pas usage de registres du processeur du tout.

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() soient dans les registres, et que les paramètres passés dans g() soient sur la pile ?

Comment sait-on que les déclarations pour x, y, z les font stocker dans les registres ? Où sont stockées les données dans g(), registre ou pile ?

Il n'est pas possible de répondre à ces deux questions de manière unique pour tous les compilateurs et systèmes sur lesquels votre code sera compilé. Elles ne peuvent même pas être considérées comme acquises puisque g Les paramètres de l'utilisateur 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 de ce que l'on appelle conventions d'appel qui définissent, entre autres, la façon dont les paramètres de la fonction sont transmis (par exemple, poussés sur la pile, placés dans des registres, ou un mélange des deux). Ceci n'est pas imposé par la norme C++ et les conventions d'appel font partie de l'architecture de la fonction ABI un sujet plus large concernant les problèmes de programmes de code machine de bas niveau.

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

Sans parler des optimisations du compilateur qui pourraient même éliminer la nécessité de la présence de certaines variables.

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

Naturellement, votre expérience peut varier, et il y a des milliards de variables qui sortent du cadre de cette FAQ particulière, mais ce qui précède est un 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 des hypothèses pour donner un exemple. Ce ne sont que des hypothèses et vous devez les traiter comme telles.

Petit complément : concernant les avantages de inline sur une fonction, je vous recommande de jeter un coup d'œil à cette réponse : http://stackoverflow.com/a/145952/1938163

5voto

Thomas Matthews Points 19838

Vous ne pouvez pas savoir, sans regarder le langage d'assemblage, si une variable se trouve dans un registre, une pile, un tas, une mémoire globale ou ailleurs. A variable est un concept abstrait. Le compilateur est autorisé à utiliser les registres ou toute autre mémoire comme il l'entend, tant que l'exécution n'est pas modifiée .

Il y a aussi 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 des variables 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 comme une constante.

3voto

Casey Points 314

En ce qui concerne votre première question, oui, les instructions autres que load/store opèrent sur les registres.

En ce qui concerne votre question n°2, si nous supposons que les paramètres sont passés sur la pile, alors nous devons écrire les registres sur la pile, sinon g() ne sera pas en mesure d'accéder aux données, puisque le code dans g() ne "sait" pas dans quels registres se trouvent les paramètres.

En ce qui concerne votre question n°3, on ne sait pas que x, y et z seront certainement stockés dans des registres dans f(). On pourrait utiliser le register mot-clé, mais c'est plutôt une suggestion. Sur la base de la convention d'appel, et en supposant que le compilateur n'effectue pas d'optimisation impliquant le passage de paramètres, vous pouvez être en mesure de 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 manière dont les paramètres sont transmis aux fonctions et impliquent généralement le passage des paramètres sur la pile dans un ordre spécifique, le placement des 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 identiques, dans la mesure où 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

En fastcall 64bit, 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() à l'aide de MS 64bit fastcall, il faut faire ce qui suit (nous supposons que) z , x et y ne sont pas dans les registres)

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

C'est ainsi que l'assemblage est écrit par les humains, et parfois par 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 de fois où la mémoire est accédée. Prenez le code suivant par exemple (j'ignore intentionnellement les règles de registre non volatile) :

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

À titre d'exemple, rcx est déjà utilisé, et x a été chargé dans rsi. Le compilateur peut compiler g de manière à ce qu'il utilise rsi au lieu de rcx, de sorte que les valeurs ne doivent pas être échangées entre les deux registres au moment d'appeler g. Le compilateur peut également mettre g en ligne, maintenant que f et g partagent le même ensemble de registres pour x, y et z. Dans ce cas, la fonction call g serait remplacée par le contenu de g, à l'exclusion de l'instruction ret l'instruction.

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'aurons pas à nous occuper de l'élément call puisque g a été inlined dans f.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X