Vediamo due piccoli programmi in C che fanno un po 'di spostamento e una divisione.
#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;
}
Questi vengono quindi compilati gcc -S
per vedere quale sarà l'assemblaggio effettivo.
Con la versione bit shift, dalla chiamata a atoi
per tornare:
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
Mentre la versione di divisione:
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
Solo guardando questo ci sono molte più istruzioni nella versione di divisione rispetto al bit shift.
La chiave è cosa fanno?
Nella versione bit shift l'istruzione chiave è shll $2, %eax
che è un turno lasciato logico: c'è la divisione e tutto il resto sta semplicemente spostando i valori.
Nella versione di divisione, puoi vedere idivl %r8d
- ma proprio sopra quello è un cltd
(converti long in double) e qualche logica aggiuntiva attorno alla fuoriuscita e alla ricarica. Questo lavoro aggiuntivo, sapendo che abbiamo a che fare con una matematica piuttosto che con i bit, è spesso necessario per evitare vari errori che possono verificarsi facendo solo un po 'di matematica.
Facciamo una rapida moltiplicazione:
#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;
}
Invece di passare attraverso tutto questo, c'è una riga diversa:
$ diff mult.s bit.s
24c24
> shll $ 2,% eax
---
<sarl $ 2,% eax
Qui il compilatore è stato in grado di identificare che la matematica poteva essere fatta con uno spostamento, tuttavia invece di uno spostamento logico fa uno spostamento aritmetico. La differenza tra questi sarebbe ovvia se li eseguessimo - sarl
preserva il segno. In modo che -2 * 4 = -8
mentre il shll
non lo fa.
Vediamo questo in un rapido script perl:
#!/usr/bin/perl
$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";
$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";
Produzione:
16
16
18446744073709551600
-16
Um ... -4 << 2
è 18446744073709551600
che non è esattamente quello che probabilmente ti aspetti quando hai a che fare con la moltiplicazione e la divisione. È giusto, ma non è una moltiplicazione intera.
E quindi diffidare dell'ottimizzazione prematura. Lascia che il compilatore ottimizzi per te: sa cosa stai davvero cercando di fare e probabilmente farà un lavoro migliore, con meno bug.