Schauen wir uns zwei kleine C-Programme an, die eine kleine Verschiebung und eine Teilung bewirken.
#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;
}
Diese werden dann jeweils kompiliert, um gcc -S
zu sehen, wie die eigentliche Baugruppe aussehen wird.
Mit der Bitverschiebungsversion vom Aufruf bis atoi
zur Rückkehr:
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
Während die Divide-Version:
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
Wenn man sich das nur ansieht, gibt es in der Divide-Version im Vergleich zur Bitverschiebung mehrere Anweisungen mehr.
Der Schlüssel ist, was machen sie?
In der Bitverschiebungsversion ist der Schlüsselbefehl shll $2, %eax
eine logische Verschiebung nach links - es gibt die Teilung, und alles andere bewegt nur Werte.
In der Divide-Version sehen Sie das idivl %r8d
- aber genau darüber befindet sich ein cltd
(Long in Double konvertieren) und eine zusätzliche Logik rund um das Verschütten und Nachladen. Diese zusätzliche Arbeit, in dem Wissen, dass es sich eher um eine Mathematik als um Bits handelt, ist häufig erforderlich, um verschiedene Fehler zu vermeiden, die nur durch Bit-Mathematik auftreten können.
Machen wir eine schnelle Multiplikation:
#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;
}
Anstatt all dies durchzugehen, gibt es eine andere Zeile:
$ diff mult.s bit.s.
24c24
> shll $ 2,% eax
--- ---.
<sarl $ 2,% eax
Hier konnte der Compiler feststellen, dass die Mathematik mit einer Verschiebung durchgeführt werden kann, jedoch anstelle einer logischen Verschiebung eine arithmetische Verschiebung. Der Unterschied zwischen diesen wäre offensichtlich, wenn wir diese ausführen würden - sarl
bewahrt das Zeichen. Also -2 * 4 = -8
das shll
tut das nicht.
Schauen wir uns das in einem kurzen Perl-Skript an:
#!/usr/bin/perl
$foo = 4;
print $foo << 2, "\n";
print $foo * 4, "\n";
$foo = -4;
print $foo << 2, "\n";
print $foo * 4, "\n";
Ausgabe:
16
16
18446744073709551600
-16
Ähm ... -4 << 2
ist 18446744073709551600
nicht genau das, was Sie wahrscheinlich erwarten, wenn Sie sich mit Multiplikation und Division befassen. Es ist richtig, aber es ist keine ganzzahlige Multiplikation.
Und seien Sie daher vorsichtig bei vorzeitiger Optimierung. Lassen Sie den Compiler für Sie optimieren - er weiß, was Sie wirklich versuchen, und wird es wahrscheinlich mit weniger Fehlern besser machen.