Przyjrzyjmy się dwóm małym programom C, które zmieniają się nieco i dzielą.
#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;
}
Następnie są one kompilowane, gcc -S
aby zobaczyć, jaki będzie rzeczywisty zestaw.
W wersji z przesunięciem bitowym od wezwania atoi
do powrotu:
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
Podczas podziału wersji:
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
Wystarczy spojrzeć na to i istnieje kilka innych instrukcji w wersji divide w porównaniu do przesunięcia bitów.
Kluczem jest to, co oni robią?
W wersji z przesunięciem bitów kluczową instrukcją jest shll $2, %eax
przesunięcie logiczne w lewo - istnieje podział, a wszystko inne przesuwa wartości.
W wersji dzielącej możesz zobaczyć idivl %r8d
- ale tuż nad tym jest cltd
(zamień długi na podwójny) i pewną dodatkową logikę wokół wycieku i przeładowania. Ta dodatkowa praca, wiedząc, że mamy do czynienia z matematyką, a nie z bitami, jest często konieczna, aby uniknąć różnych błędów, które mogą wystąpić, wykonując tylko matematykę bitową.
Zróbmy szybkie pomnożenie:
#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;
}
Zamiast przejść przez to wszystko, jest jedna linia inna:
$ diff mult.s bit.s
24c24
> 2 dolary,% eax
---
<sarl 2 $,% eax
Tutaj kompilator był w stanie stwierdzić, że matematyki można dokonać za pomocą przesunięcia, jednak zamiast przesunięcia logicznego dokonuje przesunięcia arytmetycznego. Różnica między nimi byłaby oczywista, gdybyśmy je uruchomili - sarl
zachowuje znak. Tak więc, -2 * 4 = -8
podczas gdy shll
nie.
Spójrzmy na to w szybkim skrypcie perla:
#!/usr/bin/perl
$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";
$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";
Wynik:
16
16
18446744073709551600
-16
Um ... -4 << 2
to 18446744073709551600
nie jest dokładnie to, czego się prawdopodobnie spodziewasz w przypadku mnożenia i dzielenia. Ma rację, ale nie jest mnożeniem liczb całkowitych.
I dlatego uważaj na przedwczesną optymalizację. Pozwól, aby kompilator zoptymalizował się dla Ciebie - wie, co naprawdę próbujesz zrobić i prawdopodobnie wykona to lepiej, z mniejszą liczbą błędów.