Ouvrir le menu principal

Les opérations bit à bit/Les opérations arithmétiques sur des entiers

< Les opérations bit à bit

Les opérations bit à bit permettent d'effectuer certains calculs arithmétiques assez simplement et souvent avec des performances très intéressantes. Dans ces conditions, effectuer des calculs arithmétiques avec des opérations bit à bit est un bon investissement. Dans ce qui va suivre, nous allons évidemment travailler avec des nombres en complément à deux.

Sections

MoyenneModifier

Calculer la moyenne de deux nombres peut sembler simple comme bonjour. Il suffit d'appliquer la formule  . Mais cette expression peut donner lieu à un débordement d'entier : le calcul de x + y peut dépasser la taille d'un registre, ce qui donnera un résultat totalement faux. Pour éviter cela, on peut ruser en utilisant des astuces mathématiques. Une première astuce consiste à reformuler l'équation   de manière à éviter d'additionner x et y. Si  , on sait que la moyenne se calcule comme suit :  . Mais cette expression peut encore être simplifiée en utilisant des opérations bit à bit, plus rapides.

Nombres non-signésModifier

Pour rappel, la somme de deux bits a et b donne un bit de résultat et une retenue. Le bit de résultat vaut  , tandis que la retenue vaut  . C'est exactement la même chose pour la somme de deux nombres A et B, si on prend en compte le fait que les retenues doivent être additionnés aux bits de résultats de la colonne suivante. On peut donc calculer la somme de deux nombres en calculant les bits de résultat (sans propager les retenues) et les retenues séparément : il suffit d'additionner ces deux termes pour obtenir le résultat. N'oublions pas que les retenues doivent être additionnées à la colonne suivante, ce qui fait que le terme pour les retenues doit être décalé d'un cran vers la gauche. Vu que les retenues se calculent en faisant   et les bits de résultat avec   , on a :

 

Maintenant, divisons le tout par deux pour obtenir la moyenne, ce qui revient à tout décaler d'un cran vers la droite. On obtient alors la formule pour calculer la moyenne :

 

Celle-ci peut être reformulée comme suit, en utilisant les identités sur l'addition et la soustraction du premier chapitre :

 

Voici le code source qui correspond à la première formule :

unsigned average (unsigned a , unsigned b)
{
    return ( a & b ) + ( a ^ b ) >> 1 ;
}

Nombres signésModifier

Le calcul de la moyenne pour les nombres signés peut se faire avec les formules précédentes, à condition de remplacer le décalage par un décalage arithmétique. Mais dans ce cas, il faut faire attention aux arrondis pour les nombres négatifs. Autant cela ne pose aucun problème pour les nombres positifs, autant l'arrondi des nombres négatifs est quelque peu erroné. Par exemple, la moyenne de -5 et -2 peut être arrondie de deux manières : soit vers 0, ce qui donne -2, soit vers   ce qui donne -3. Les calculs précédents vont causer un arrondi vers  . Pour les nombres positifs, ces deux arrondis sont identiques, mais donnent des résultats différents pour les négatifs. Pour obtenir un arrondi vers 0, les formules de la section précédente doivent être modifiées. Cette correction est simple : si x + y est négatif et impair, on l'incrémente. Pour cela, on peut prendre le bit de signe du résultat x + y ou celui de la moyenne, ces deux bits étant identiques. Pour un nombre de n bits, celui-ci se calcule avec la formule  . Une fois ce bit de signe obtenu, il faut l'additionner seulement si x + y est impair. Déterminer si la somme de deux nombres est impair est simple : le bit de poids faible doit être égal à 1. Il peut se calculer avec un XOR entre les deux opérandes à moyenner. Une fois qu'on a le bit de poids faible et le bit de signe localisés sur la même colonne, il suffit de faire un ET entre les deux : le résultat de l’opération donne un bit qui vaut 1 si la moyenne est impaire et négative, et 0 sinon. Il reste à additionner ce bit à la moyenne pour la corriger.

 

 

 

Multiplication par une constanteModifier

Les opérations de multiplication entière sont gourmandes en temps d’exécution et toute optimisation est bonne à prendre. Les compilateurs modernes sont capables d'optimiser un grand nombre de multiplications pour les remplacer par des opérations plus rapides. Cela arrive souvent lorsqu'on utilise des tableaux : les calculs d'adresses localisés dans des boucles peuvent parfois être optimisés et certaines multiplications sont alors remplacées par des additions. À ce petit jeu, la multiplication par une constante est une opportunité d'optimisation importante. On a vu précédemment qu'il est possible de remplacer la multiplication par une puissance de deux par un simple décalage. Ce qui va suivre se base sur ce principe, même si nous allons aborder la multiplication par autre chose qu'une puissance de deux. Aussi bizarre que cela puisse paraitre, il y a deux méthodes pour cela. Suivant la situation, l'une d'entre elle sera plus rapide que l'autre : tout dépend du nombre de bits à 1 dans la constante.

Décaler et additionnerModifier

Comme vous le savez, tout nombre entier est une somme de puissances de deux. C'est d'ailleurs le principe qui est derrière le binaire. Dans ce cas, multiplier par une puissance, c'est multiplier par une somme de puissances de deux. Avec quelques manipulations algébriques simples, cette multiplication peut se transformer en une somme de multiplications par une puissance de deux. Supposons que je veuille effectuer une multiplication entre un nombre A, et une constante B. Il est évident que B est une somme de puissances de deux. Dans ce cas, je peux remplacer B par la somme de puissances de deux qui correspond. Dans ce qui va suivre, nous allons prendre B = 13. Pour rappel,   :

 

En utilisant la distributivité de la multiplication, on trouve :

 

Dans cette expression, on a donc une somme, qui additionne quelques termes. Ces termes sont tous des multiplications par une puissance de deux. On peut les remplacer par un décalage vers la gauche.

 

Ainsi, la multiplication par 13 peut se remplacer en deux décalages et deux additions.

Le principe reste le même pour toute multiplication par une constante : en décomposant la constante en puissances de deux, et avec quelques calculs, on peut transformer une multiplication par une constante en série de décalages et d'additions. Le nombre de décalages effectué est égal au nombre de bits à 1 dans la constante. Le nombre d'additions est presque identique. Si la constante contient donc trop de bits à 1, il se peut que le nombre de décalages et d'additions soit trop important : une multiplication peut être plus rapide. Cette technique ne marche donc que pour les nombres ayant un nombre de bits à 1 faible : 2-3 bits, parfois plus sur les architectures ayant une multiplication particulièrement lente, mais pas plus.

Décaler et soustraireModifier

Dans le cas où un nombre contient beaucoup de bits à 1, il existe une seconde méthode. Elle se base sur un principe légèrement différent, mais assez similaire à celui utilisé dans la méthode précédente. Comme vous le savez, un nombre entier peut s'écrire sous la forme d'une somme de puissances de deux. Par exemple,  . Ceci dit, 5 peut aussi s'écrire sous une autre forme. Si on remarque bien,  . Dans cette expression, 8 est une puissance de 2, et 3 est un nombre comme un autre. Si on réfléchit bien, tout nombre entier peut s'écrire sous la forme  .

Prenons un exemple avec une multiplication par 5.

 

Dans cette expression, on peut remplacer chaque nombre par son expression en binaire. En clair, on le remplace par la somme de puissances de deux qui correspond.

 

 

Maintenant, supposons que l'on veuille multiplier un nombre A par 5. On peut alors remplacer 5 par l'expression avec décalages et soustractions :

 

En se rappelant que la multiplication est distributive, on obtient :

 

Dans cette expression, on peut alors remplacer les multiplications par des puissances de deux par des décalages à gauche :

 

Comme on le voit, cette expression ne contient que des décalages et des soustractions. Et le principe est le même pour tout entier. Il suffit d'écrire la constante comme une puissance de deux à laquelle on aurait soustrait ce qu'il faut. Pour cela, il suffit de prendre la puissance de deux immédiatement supérieure à notre constante : cela simplifie les calculs et diminue le nombre de soustractions. On a donc l'équation : constante + nombre à soustraire = puissance de deux choisie. Si on calcule sur n bits, la puissance de deux aura n+1 bits et vaudra donc zéro. En clair : constante + nombre à soustraire = 0. Ce qui signifie que constante = - nombre à soustraire : le nombre à soustraire est donc le complément à deux de la constante, codé sur n bits. Cela nous dit donc que plus un nombre à de bits à 1, plus son complément à deux aura de bits à 0 : le nombre de soustractions à effectuer sera donc plus faible. Cette technique marche donc nettement mieux pour les nombres remplis de 1.

Division par une constanteModifier

Après la multiplication par une constante, il faut savoir que la division par une constante peut aussi être améliorée. On sait déjà ce qu'il en est pour la division par une puissance de deux : il suffit de la remplacer par un décalage vers la droite, en prenant garde à bien choisir entre décalage arithmétique et logique. Pour ce qui est de la division par une constante arbitraire, il est possible d'utiliser une technique d'optimisation particulièrement efficace : la multiplication réciproque. Cette technique nous permet de remplacer la division par une constante en une multiplication. Les divisions étant des opérations nettement plus lentes que les multiplications, on peut facilement gagner quelques dizaines de cycles d'horloge.

Mais tout d'abord, nous devons préciser une chose. Lorsqu'on multiplie deux nombres de   bits, leur produit a une taille qui est de   bits. Dans le détail, on a besoin de deux fois plus de bits pour le résultat. D'ordinaire, on s'en moque, et on se contente de garder les   bits de poids faible. Mais dans ce qui va suivre, nous allons avoir besoin des   bits de poids fort. Heureusement, certains processeurs disposent d'instructions de multiplications qui calculent ces bits de poids fort, le résultat étant enregistré dans deux registres : un pour les bits de poids faible, un autre pour les registres de poids forts. D'autres processeurs disposent d'une instruction qui effectue la multiplication et ne conserve que les bits de poids forts dans un registre. Enfin, certains processeurs disposent d'instructions de multiplication configurables, qui permettent de choisir quels bits conserver : poids fort ou poids faible.

La multiplication réciproque est basée sur un principe simple : diviser par  , c'est multiplier par  . Si N est une constante, alors on peut pré-calculer   et éliminer ainsi la division. Et là, un premier problème semble se faire jour : cela marche peut-être pour les nombres flottants, mais ne peut pas fonctionner avec des nombres entiers. Mais on peut sauver le principe en remplaçant   par une constante judicieusement choisie, dont la multiplication donnera le résultat voulu. Si on multiplie par cette constante et que l'on garde les bits de poids forts, on obtiendra le même résultat qu'avec une multiplication par   (sur les bits de poids faible). Cette constante vaut, par définition :  . En effet, regardons ce que donne l’opération suivante, avec A le nombre qu'on cherche à diviser par N :

 

 

 

En clair, on obtient le résultat de la division de A par N, mais décalé de n rangs : on obtient bien le résultat de la division dans les bits de poids fort.

Cette méthode demande que le calcul de   tombe juste, qu'il n'y aie pas de part fractionnaire. Mais ce n'est pas le cas pour certaines constantes, notamment N = 10, 7, 11, et grosso-modo, tout ce qui n'est pas une puissance de deux. Dans ce cas, le mieux est de rechercher des constantes proches de la constante réciproque qui donnent de meilleurs résultats. Cependant, l'imprécision de la constante réciproque est alors à l'origine d'erreurs de calcul et le résultat doit être corrigé avant d'être utilisable. Cette correction est une simple addition, qui ajoute un paramètre déterminé, fixe, qui dépend de la constante.