Regardons deux petits programmes C qui font un peu de décalage et une division.
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int b = i << 2;
}
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int d = i / 4;
}
Celles-ci sont ensuite compilées avec chacune gcc -S
pour voir quel sera l'assemblage réel.
Avec la version bit shift, de l'appel atoi
au retour:
callq _atoi
movl $0, %ecx
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
shll $2, %eax
movl %eax, -24(%rbp)
movl %ecx, %eax
addq $32, %rsp
popq %rbp
ret
Alors que la version divisée:
callq _atoi
movl $0, %ecx
movl $4, %edx
movl %eax, -20(%rbp)
movl -20(%rbp), %eax
movl %edx, -28(%rbp) ## 4-byte Spill
cltd
movl -28(%rbp), %r8d ## 4-byte Reload
idivl %r8d
movl %eax, -24(%rbp)
movl %ecx, %eax
addq $32, %rsp
popq %rbp
ret
Juste en regardant cela, il y a plusieurs autres instructions dans la version de division par rapport au décalage de bits.
La clé est que font-ils?
Dans la version à décalage de bits, l'instruction clé est la shll $2, %eax
logique de décalage à gauche - il y a le fossé, et tout le reste ne fait que déplacer des valeurs.
Dans la version diviser, vous pouvez voir le idivl %r8d
- mais juste au-dessus de cela est un cltd
(convertir long en double) et une logique supplémentaire autour du déversement et du rechargement. Ce travail supplémentaire, sachant que nous avons affaire à des mathématiques plutôt qu'à des bits, est souvent nécessaire pour éviter diverses erreurs qui peuvent se produire en effectuant uniquement des calculs de bits.
Permet de faire une multiplication rapide:
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int b = i >> 2;
}
#include <stdlib.h>
int main(int argc, char* argv[]) {
int i = atoi(argv[0]);
int d = i * 4;
}
Plutôt que de passer par tout cela, il y a une ligne différente:
$ diff mult.s bit.s
24c24
> shll $ 2,% eax
---
<sarl $ 2,% eax
Ici, le compilateur a pu identifier que le calcul pouvait être effectué avec un décalage, mais au lieu d'un décalage logique, il effectue un décalage arithmétique. La différence entre ceux-ci serait évidente si nous les exécutions - sarl
préserve le signe. Alors que bien -2 * 4 = -8
que le shll
ne soit pas.
Voyons cela dans un script perl rapide:
#!/usr/bin/perl
$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";
$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";
Production:
16
16
18446744073709551600
-16
Um ... -4 << 2
n'est 18446744073709551600
pas exactement ce à quoi vous vous attendez probablement en matière de multiplication et de division. C'est vrai, mais ce n'est pas une multiplication entière.
Et donc méfiez-vous de l'optimisation prématurée. Laissez le compilateur optimiser pour vous - il sait ce que vous essayez vraiment de faire et fera probablement un meilleur travail avec moins de bogues.