Vamos olhar para dois pequenos programas C que mudam e dividem um pouco.
#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;
}
Estes são então compilados gcc -S
para ver qual será a montagem real.
Com a versão de troca de bits, da chamada atoi
para retornar:
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
Enquanto a versão dividida:
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
Só de olhar para isso, há várias outras instruções na versão dividida em comparação com a mudança de bits.
A chave é o que eles fazem?
Na versão de deslocamento de bits, a instrução principal é shll $2, %eax
qual é o deslocamento deixado lógico - existe a divisão, e todo o resto está apenas movendo valores.
Na versão dividida, você pode ver o idivl %r8d
- mas logo acima disso há uma cltd
(converter muito para o dobro) e alguma lógica adicional em torno do derramamento e do recarregamento. Esse trabalho adicional, sabendo que estamos lidando com uma matemática em vez de bits, é frequentemente necessário para evitar vários erros que podem ocorrer ao fazer apenas a matemática de bits.
Vamos fazer uma multiplicação rápida:
#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;
}
Em vez de passar por tudo isso, há uma linha diferente:
$ diff mult.s bit.s
24c24
> $ 2,% eax
---
<sarl $ 2,% eax
Aqui, o compilador foi capaz de identificar que a matemática poderia ser feita com uma mudança, no entanto, em vez de uma mudança lógica, ela faz uma mudança aritmética. A diferença entre estes seria óbvia se os executássemos - sarl
preserva o sinal. Então, -2 * 4 = -8
enquanto shll
isso não acontece.
Vamos analisar isso em um script perl rápido:
#!/usr/bin/perl
$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";
$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";
Resultado:
16
16
18446744073709551600
-16
Um ... -4 << 2
é o 18446744073709551600
que não é exatamente o que você provavelmente espera ao lidar com multiplicação e divisão. Está certo, mas não é uma multiplicação inteira.
E, portanto, tenha cuidado com a otimização prematura. Deixe o compilador otimizar para você - ele sabe o que realmente está tentando fazer e provavelmente fará um trabalho melhor com menos bugs.