web-dev-qa-db-fra.com

Comment fonctionne l'opérateur + en C?

Pour comprendre comment les opérateurs primitifs tels que +, -, * et / sont implémentés en C, j'ai trouvé l'extrait suivant de ne réponse intéressante .

// replaces the + operator
int add(int x, int y) {
    while(x) {
        int t = (x & y) <<1;
        y ^= x;
        x = t;
    }
    return y;
}

Semble que cette fonction montre comment + fonctionne réellement en arrière-plan. Cependant, c'est trop déroutant pour moi de le comprendre. Je pensais que de telles opérations sont effectuées à l'aide de directives d'assemblage générées par le compilateur pendant longtemps!

Ma question est: le + opérateur implémenté comme le code publié sur [~ # ~] la plupart des implémentations [~ # ~] ? Est-ce que cela profite du complément à deux ou d'autres fonctionnalités dépendant de l'implémentation? Et je l'apprécierai beaucoup si quelqu'un peut expliquer comment cela fonctionne.

Hmm ... Peut-être que cette question est un peu hors sujet sur SO, mais je suppose que c'est un peu bien de regarder à travers ces opérateurs.

80
nalzok

Pour être pédant, la spécification C ne spécifie pas comment l'addition est implémentée.

Mais pour être réaliste, le + L'opérateur sur les types entiers inférieurs ou égaux à la taille Word de votre CPU est traduit directement en une instruction d'addition pour le CPU, et les types entiers plus grands sont traduits en plusieurs instructions d'addition avec quelques bits supplémentaires pour gérer le débordement.

Le CPU utilise en interne des circuits logiques pour implémenter l'addition, et n'utilise pas de boucles, de décalages de bits ou quoi que ce soit qui ressemble étroitement au fonctionnement de C.

184
orlp

Lorsque vous ajoutez deux bits, le résultat est le suivant: (table de vérité)

a | b | sum (a^b) | carry bit (a&b) (goes to next)
--+---+-----------+--------------------------------
0 | 0 |    0      | 0
0 | 1 |    1      | 0
1 | 0 |    1      | 0
1 | 1 |    0      | 1

Donc, si vous faites xor au niveau du bit, vous pouvez obtenir la somme sans report. Et si vous le faites au niveau du bit et que vous pouvez obtenir les bits de retenue.

Extension de cette observation aux nombres multibits a et b

a+b = sum_without_carry(a, b) + carry_bits(a, b) shifted by 1 bit left
    = a^b + ((a&b) << 1)

Une fois que b est 0:

a+0 = a

Donc, l'algorithme se résume à:

Add(a, b)
  if b == 0
    return a;
  else
    carry_bits = a & b;
    sum_bits = a ^ b;
    return Add(sum_bits, carry_bits << 1);

Si vous vous débarrassez de la récursivité et la convertissez en boucle

Add(a, b)
  while(b != 0) {
    carry_bits = a & b;
    sum_bits = a ^ b;

    a = sum_bits;
    b = carrry_bits << 1;  // In next loop, add carry bits to a
  }
  return a;

Avec l'algorithme ci-dessus à l'esprit, l'explication du code devrait être plus simple:

int t = (x & y) << 1;

Portez des morceaux. Carry bit est 1 si 1 bit vers la droite dans les deux opérandes vaut 1.

y ^= x;  // x is used now

Addition sans report (bits de transport ignorés)

x = t;

Réutilisez x pour le configurer pour le transporter

while(x)

Répéter pendant qu'il y a plus de bits de retenue


Une implémentation récursive (plus facile à comprendre) serait:

int add(int x, int y) {
    return (y == 0) ? x : add(x ^ y, (x&y) << 1);
}

Semble que cette fonction montre comment + fonctionne réellement en arrière-plan

Non. Habituellement (presque toujours) l'addition entière se traduit par l'ajout d'instructions machine. Cela montre simplement une implémentation alternative en utilisant xor et et au niveau du bit.

77
Mohit Jain

Semble que cette fonction montre comment + fonctionne réellement en arrière-plan

Non. Ceci est traduit par l'instruction machine native add, qui utilise en fait l'additionneur matériel, dans le ALU.

Si vous vous demandez comment l'ordinateur ajoute, voici un additionneur de base.

Tout dans l'ordinateur se fait à l'aide de portes logiques, qui sont principalement constituées de transistors. L'additionneur complet contient des demi-additionneurs.

Pour un didacticiel de base sur les portes logiques et les additionneurs, voir this . La vidéo est extrêmement utile, bien que longue.

Dans cette vidéo, un demi-additionneur de base est montré. Si vous voulez une brève description, c'est celle-ci:

Les deux bits du demi-additionneur sont donnés. Les combinaisons possibles sont:

  • Ajouter 0 et 0 = 0
  • Ajouter 1 et 0 = 1
  • Ajouter 1 et 1 = 10 (binaire)

Alors maintenant, comment fonctionne le demi-additionneur? Eh bien, il est composé de trois portes logiques, le and, xor et le nand. Le nand donne un courant positif si les deux entrées sont négatives, ce qui signifie que cela résout le cas de 0 et 0. Le xor donne une sortie positive l'une des entrées est positive, et le autre négatif, ce qui signifie qu'il résout le problème de 1 et 0. Le and donne une sortie positive uniquement si les deux entrées sont positives, de sorte que résout le problème de 1 et 1. Donc, fondamentalement, nous avons maintenant notre demi-additionneur. Mais nous ne pouvons toujours ajouter que des bits.

Maintenant, nous faisons notre additionneur complet. Un additionneur complet consiste à appeler le demi-additionneur encore et encore. Maintenant, cela a un report. Lorsque nous ajoutons 1 et 1, nous obtenons un report 1. Donc, ce que fait l'additionneur complet, il prend le report du demi-additionneur, le stocke et le passe comme un autre argument au demi-additionneur.

Si vous ne savez pas comment passer le report, vous devez d'abord ajouter les bits à l'aide du demi-additionneur, puis ajouter la somme et le report. Alors maintenant, vous avez ajouté le report, avec les deux bits. Donc, vous faites cela encore et encore, jusqu'à ce que les bits que vous devez ajouter soient terminés, puis vous obtenez votre résultat.

Surpris? Voilà comment cela se passe réellement. Cela ressemble à un long processus, mais l'ordinateur le fait en quelques fractions de nanosecondes, ou pour être plus précis, en un demi-cycle d'horloge. Parfois, il est effectué même en un seul cycle d'horloge. Fondamentalement, l'ordinateur a le ALU (une grande partie du CPU), la mémoire, les bus, etc.

Si vous voulez apprendre le matériel informatique, à partir des portes logiques, de la mémoire et de l'ALU, et simuler un ordinateur, vous pouvez voir ce cours, à partir duquel j'ai appris tout cela: Construire un ordinateur moderne à partir des premiers principes

C'est gratuit si vous ne voulez pas de certificat électronique. La deuxième partie du cours arrive au printemps de cette année

25
Ashish Ahuja

C utilise une machine abstraite pour décrire ce que fait le code C. Donc, comment cela fonctionne n'est pas spécifié. Il existe par exemple des "compilateurs" C qui compilent C dans un langage de script.

Mais, dans la plupart des implémentations C, + entre deux entiers inférieurs à la taille entière de la machine sera traduit en une instruction d'assemblage (après plusieurs étapes). L'instruction d'assemblage sera traduite en code machine et intégrée dans votre exécutable. L'assemblage est un langage "une étape supprimé" du code machine, destiné à être plus facile à lire qu'un tas de binaires compressés.

Ce code machine (après plusieurs étapes) est ensuite interprété par la plate-forme matérielle cible, où il est interprété par le décodeur d'instructions sur le CPU. Ce décodeur d'instructions prend l'instruction et la traduit en signaux à envoyer le long de "lignes de contrôle". Ces signaux acheminent les données des registres et de la mémoire à travers le CPU, où les valeurs sont souvent additionnées dans une unité logique arithmétique.

L'unité logique arithmétique peut avoir des additionneurs et des multiplicateurs séparés, ou peut les mélanger.

L'unité logique arithmétique a un tas de transistors qui effectuent l'opération d'addition, puis produisent la sortie. Cette sortie est acheminée via les signaux générés par le décodeur d'instructions et stockée dans la mémoire ou les registres.

La disposition desdits transistors dans l'unité de logique arithmétique et le décodeur d'instructions (ainsi que les parties que j'ai passées sous silence) est gravée dans la puce de l'usine. Le motif de gravure est souvent produit en compilant un langage de description matérielle, qui prend une abstraction de ce qui est connecté à quoi et comment ils fonctionnent et génère des transistors et des lignes d'interconnexion.

Le langage de description du matériel peut contenir des décalages et des boucles qui ne décrivent pas les événements qui se produisent dans le temps (comme l'un après l'autre) mais plutôt dans l'espace - il décrit les connexions entre les différentes parties du matériel. Ce code peut ressembler très vaguement au code que vous avez publié ci-dessus.

Ce qui précède couvre de nombreuses parties et couches et contient des inexactitudes. C'est à la fois de ma propre incompétence (j'ai écrit à la fois du matériel et des compilateurs, mais je ne suis un expert dans aucun des deux) et parce que tous les détails prendraient une carrière ou deux, et non un article SO.

Ici est un article SO sur un additionneur 8 bits. Ici est un article non SO, où vous noterez certains des les additionneurs utilisent simplement operator+ dans le HDL! (Le HDL lui-même comprend + et génère le code additionneur de niveau inférieur pour vous).

15

Le code que vous avez trouvé essaie d'expliquer comment le matériel informatique très primitif pourrait implémenter une instruction "ajouter". Je dis "pourrait" parce que je peux garantir que la méthode this n'est pas utilisée par any CPU, et je vais expliquer pourquoi.

Dans la vie normale, vous utilisez des nombres décimaux et vous avez appris à les ajouter: pour ajouter deux nombres, vous ajoutez les deux chiffres les plus bas. Si le résultat est inférieur à 10, vous notez le résultat et passez à la position de chiffre suivante. Si le résultat est de 10 ou plus, vous écrivez le résultat moins 10, passez au chiffre suivant, achetez, n'oubliez pas d'en ajouter 1 de plus. Par exemple: 23 + 37, vous ajoutez 3 + 7 = 10, vous écrivez 0 et n'oubliez pas d'en ajouter 1 de plus pour la position suivante. À la position 10, vous ajoutez (2 + 3) + 1 = 6 et notez cela. Le résultat est de 60.

Vous pouvez faire exactement la même chose avec des nombres binaires. La différence est que les seuls chiffres sont 0 et 1, donc les seules sommes possibles sont 0, 1, 2. Pour un nombre de 32 bits, vous géreriez une position de chiffre après l'autre. Et c'est ainsi que le matériel informatique vraiment primitif le ferait.

Ce code fonctionne différemment. Vous savez que la somme de deux chiffres binaires est 2 si les deux chiffres sont 1. Donc, si les deux chiffres sont 1, vous ajouteriez 1 de plus à la position binaire suivante et noteriez 0. C'est ce que fait le calcul de t: il trouve tous les endroits où les deux chiffres binaires sont 1 (c'est le &) et les déplace vers la position du chiffre suivant (<< 1). Ensuite, il fait l'addition: 0 + 0 = 0, 0 + 1 = 1, 1 + 0 = 1, 1 + 1 est 2, mais nous écrivons 0. C'est ce que fait l'exclusif ou l'opérateur.

Mais tous les 1 que vous avez dû gérer à la position du chiffre suivant n'ont pas été traités. Ils doivent encore être ajoutés. C'est pourquoi le code fait une boucle: dans la prochaine itération, tous les 1 supplémentaires sont ajoutés.

Pourquoi aucun processeur ne procède-t-il de cette façon? Parce que c'est une boucle, et les processeurs n'aiment pas les boucles, et c'est lent. C'est lent, car dans le pire des cas, 32 itérations sont nécessaires: si vous ajoutez 1 au nombre 0xffffffff (32 1 bits), la première itération efface le bit 0 de y et définit x à 2. La deuxième itération efface le bit 1 de y et définit x à 4. Et ainsi de suite. Il faut 32 itérations pour obtenir le résultat. Cependant, chaque itération doit traiter tous les bits de x et y, ce qui nécessite beaucoup de matériel.

Un processeur primitif ferait les choses tout aussi rapidement dans la façon dont vous effectuez l'arithmétique décimale, de la position la plus basse à la plus élevée. Cela prend également 32 étapes, mais chaque étape ne traite que deux bits plus une valeur de la position de bit précédente, il est donc beaucoup plus facile à mettre en œuvre. Et même dans un ordinateur primitif, on peut se permettre de le faire sans avoir à implémenter des boucles.

Un processeur moderne, rapide et complexe utilisera un "additionneur de somme conditionnelle". Surtout si le nombre de bits est élevé, par exemple un additionneur 64 bits, cela fait gagner beaucoup de temps.

Un additionneur 64 bits se compose de deux parties: Premièrement, un additionneur 32 bits pour le 32 bits le plus bas. Cet additionneur de 32 bits produit une somme et un "carry" (un indicateur qu'un 1 doit être ajouté à la position de bit suivante). Deuxièmement, deux additionneurs 32 bits pour les 32 bits supérieurs: l'un ajoute x + y, l'autre ajoute x + y + 1. Les trois additionneurs fonctionnent en parallèle. Ensuite, lorsque le premier additionneur a produit son report, le processeur sélectionne simplement lequel des deux résultats x + y ou x + y + 1 est le bon, et vous avez le résultat complet. Ainsi, un additionneur 64 bits ne prend qu'un tout petit peu plus longtemps qu'un additionneur 32 bits, pas deux fois plus longtemps.

Les parties d'additionneur 32 bits sont à nouveau implémentées comme des additionneurs de somme conditionnelle, en utilisant plusieurs additionneurs de 16 bits, et les additionneurs de 16 bits sont des additionneurs de somme conditionnelle, et ainsi de suite.

14
gnasher729

Presque tous les processeurs modernes capables d'exécuter du code C compilé auront un support intégré pour l'ajout d'entiers. Le code que vous avez publié est un moyen astucieux d'effectuer l'ajout d'entiers sans exécuter d'opcode add entier, mais ce n'est pas ainsi que l'ajout d'entiers est normalement effectué. En fait, la liaison de fonction utilise probablement une forme d'addition entière pour ajuster le pointeur de pile.

Le code que vous avez publié repose sur l'observation que lorsque vous ajoutez x et y, vous pouvez le décomposer en les bits qu'ils ont en commun et les bits qui sont uniques à l'un de x ou y.

L'expression x & y (ET au niveau du bit) donne les bits communs à x et y. L'expression x ^ y (OU exclusif au niveau du bit) donne les bits qui sont uniques à l'un de x ou y.

La somme x + y peut être réécrit comme la somme de deux fois les bits qu'ils ont en commun (puisque x et y contribuent tous les deux) plus les bits qui sont uniques à x ou y.

(x & y) << 1 est le double des bits qu'ils ont en commun (le décalage gauche de 1 se multiplie effectivement par deux).

x ^ y est les bits qui sont uniques à l'un des x ou y.

Donc, si nous remplaçons x par la première valeur et y par la seconde, la somme doit rester inchangée. Vous pouvez considérer la première valeur comme le transport des ajouts au niveau du bit et la seconde comme le bit de poids faible des ajouts au niveau du bit.

Ce processus se poursuit jusqu'à ce que x soit nul, point auquel y détient la somme.

14
Tom Karzes

Ma question est la suivante: l'opérateur + est-il implémenté comme le code affiché sur les implémentations MOST?

Répondons à la vraie question. Tous les opérateurs sont implémentés par le compilateur comme une structure de données interne qui est finalement traduite en code après quelques transformations. Vous ne pouvez pas dire quel code sera généré par un seul ajout, car presque aucun compilateur réel ne génère de code pour des instructions individuelles.

Le compilateur est libre de générer n'importe quel code tant qu'il se comporte comme si les opérations réelles ont été effectuées conformément à la norme. Mais ce qui se passe réellement peut être quelque chose de complètement différent.

Un exemple simple:

static int
foo(int a, int b)
{
    return a + b;
}
[...]
    int a = foo(1, 17);
    int b = foo(x, x);
    some_other_function(a, b);

Il n'est pas nécessaire de générer des instructions d'ajout ici. Il est parfaitement légal pour le compilateur de traduire ceci en:

some_other_function(18, x * 2);

Ou peut-être que le compilateur remarque que vous appelez la fonction foo plusieurs fois de suite et que c'est une simple arithmétique et qu'elle générera des instructions vectorielles pour elle. Ou que le résultat de l'addition est utilisé pour l'indexation de tableau ultérieurement et que l'instruction lea sera utilisée.

Vous ne pouvez tout simplement pas parler de la façon dont un opérateur est implémenté car il n'est presque jamais utilisé seul.

13
Art

Si une panne du code aide quelqu'un d'autre, prenez l'exemple x=2, y=6:


x n'est pas nul, alors commencez à ajouter à y:

while(2) {

x & y = 2 car

        x: 0 0 1 0  //2
        y: 0 1 1 0  //6
      x&y: 0 0 1 0  //2

2 <<1 = 4 car << 1 décale tous les bits vers la gauche:

      x&y: 0 0 1 0  //2
(x&y) <<1: 0 1 0 0  //4

En résumé, cachez ce résultat, 4, dans t avec

int t = (x & y) <<1;

Maintenant, appliquez le XOR au niveau du bity^=x:

        x: 0 0 1 0  //2
        y: 0 1 1 0  //6
     y^=x: 0 1 0 0  //4

Alors x=2, y=4. Enfin, somme t+y en réinitialisant x=t et en remontant au début de la boucle while:

x = t;

Quand t=0 (ou, au début de la boucle, x=0), en finir avec

return y;
11
user1717828

Juste par intérêt, sur le processeur Atmega328P, avec le compilateur avr-g ++, le code suivant implémente l'ajout d'un en soustrayant -1:

volatile char x;
int main ()
  {
  x = x + 1;  
  }

Code généré:

00000090 <main>:
volatile char x;
int main ()
  {
  x = x + 1;  
  90:   80 91 00 01     lds r24, 0x0100
  94:   8f 5f           subi    r24, 0xFF   ; 255
  96:   80 93 00 01     sts 0x0100, r24
  }
  9a:   80 e0           ldi r24, 0x00   ; 0
  9c:   90 e0           ldi r25, 0x00   ; 0
  9e:   08 95           ret

Notez en particulier que l'ajout est effectué par l'instruction subi (soustraire la constante du registre) où 0xFF est effectivement -1 dans ce cas.

Il est également intéressant de noter que ce processeur particulier n'a pas d'instruction addi, ce qui implique que les concepteurs pensaient que faire une soustraction du complément serait correctement géré par les rédacteurs du compilateur.

Est-ce que cela profite du complément à deux ou d'autres fonctionnalités dépendant de l'implémentation?

Il serait probablement juste de dire que les rédacteurs de compilateurs tenteront d'implémenter l'effet souhaité (en ajoutant un nombre à un autre) de la manière la plus efficace possible pour cette architecture en particulier. Si cela nécessite de soustraire le complément, tant pis.

11
Nick Gammon