Wie immer hängt es vom umgebenden Codekontext ab : Verwenden Sie z. x<<1
B. einen Array-Index? Oder es zu etwas anderem hinzufügen? In beiden Fällen kleine Verschiebung Zählungen (1 oder 2) kann oft optimize sogar mehr , als wenn die Compiler Enden bis zu mit nur verschieben muss. Ganz zu schweigen vom Kompromiss zwischen Durchsatz und Latenz und Front-End-Engpässen. Die Leistung eines winzigen Fragments ist nicht eindimensional.
Eine Hardware-Shift-Anweisung ist nicht die einzige Option eines Compilers zum Kompilieren x<<1
, aber die anderen Antworten gehen meistens davon aus.
x << 1
ist genau gleichbedeutend mit x+x
für vorzeichenlose und für 2-Komplement-vorzeichenbehaftete Ganzzahlen. Compiler wissen beim Kompilieren immer, auf welche Hardware sie abzielen, damit sie solche Tricks nutzen können.
Auf Intel Haswell , add
verfügt über 4 pro Takt Durchsatz, aber shl
mit einer sofortigen Zählung hat nur 2 pro Takt Durchsatz. (Sehen Anweisungen und andere Links finden http://agner.org/optimize/x86Tag Wiki). SIMD-Vektorverschiebungen betragen 1 pro Takt (2 in Skylake), aber SIMD-Vektor-Integer-Additionen betragen 2 pro Takt (3 in Skylake). Die Latenz ist jedoch dieselbe: 1 Zyklus.
Es gibt auch eine spezielle Shift-by-One-Codierung, bei der angegeben wird, shl
wo die Anzahl im Opcode enthalten ist. 8086 hatte keine Schichten mit sofortiger Zählung, nur nacheinander und nach cl
Register. Dies ist hauptsächlich für Rechtsverschiebungen relevant, da Sie nur für Linksverschiebungen hinzufügen können, es sei denn, Sie verschieben einen Speicheroperanden. Wenn der Wert jedoch später benötigt wird, ist es besser, zuerst in ein Register zu laden. Aber trotzdem shl eax,1
oder add eax,eax
ist ein Byte kürzer als shl eax,10
, und die Codegröße kann direkt (Decodierungs- / Front-End-Engpässe) oder indirekt (L1I-Code-Cache-Fehler) die Leistung beeinträchtigen.
Im Allgemeinen können kleine Verschiebungszahlen manchmal in einem Adressierungsmodus auf x86 in einen skalierten Index optimiert werden. Die meisten anderen heutzutage gebräuchlichen Architekturen sind RISC-Architekturen und verfügen nicht über Adressierungsmodi für skalierte Indizes. X86 ist jedoch eine Architektur, die häufig genug ist, um dies zu erwähnen. (Ei, wenn Sie ein Array von 4-Byte-Elementen indizieren, können Sie den Skalierungsfaktor um 1 erhöhen int arr[]; arr[x<<1]
).
Das Kopieren + Verschieben ist in Situationen üblich, in denen der ursprüngliche Wert von x
noch benötigt wird. Die meisten x86-Integer-Anweisungen werden jedoch direkt ausgeführt. (Das Ziel ist eine der Quellen für Anweisungen wie add
oder shl
.) Die x86-64-System V-Aufrufkonvention übergibt Argumente in Registern mit dem ersten Argument in edi
und dem Rückgabewert in eax
, sodass x<<10
der Compiler bei einer zurückgegebenen Funktion auch copy + shift ausgibt Code.
Mit der LEA
Anweisung können Sie verschieben und hinzufügen (mit einer Verschiebungsanzahl von 0 bis 3, da die Maschinencodierung im Adressierungsmodus verwendet wird). Das Ergebnis wird in einem separaten Register abgelegt.
gcc und clang optimieren diese Funktionen auf dieselbe Weise, wie Sie im Godbolt-Compiler-Explorer sehen können :
int shl1(int x) { return x<<1; }
lea eax, [rdi+rdi] # 1 cycle latency, 1 uop
ret
int shl2(int x) { return x<<2; }
lea eax, [4*rdi] # longer encoding: needs a disp32 of 0 because there's no base register, only scaled-index.
ret
int times5(int x) { return x * 5; }
lea eax, [rdi + 4*rdi]
ret
int shl10(int x) { return x<<10; }
mov eax, edi # 1 uop, 0 or 1 cycle latency
shl eax, 10 # 1 uop, 1 cycle latency
ret
LEA mit 2 Komponenten hat eine Latenz von 1 Zyklus und einen Durchsatz von 2 pro Takt auf neueren Intel- und AMD-CPUs. (Sandybridge-Familie und Bulldozer / Ryzen). Unter Intel ist es nur 1 Durchsatz pro Takt mit 3c Latenz für lea eax, [rdi + rsi + 123]
. (Siehe auch : Warum ist der C ++ Code schneller als meine handschriftliche Versammlung für die Vermutung Collatz testen? Geht in dieser im Detail.)
Auf jeden Fall benötigt Kopieren + Verschieben um 10 eine separate mov
Anweisung. Bei vielen neueren CPUs ist die Latenz möglicherweise null, es werden jedoch immer noch Front-End-Bandbreite und Codegröße benötigt. ( Kann der MOV von x86 wirklich "kostenlos" sein? Warum kann ich das überhaupt nicht reproduzieren? )
Ebenfalls verwandt: Wie multipliziere ich ein Register mit 37 mit nur 2 aufeinanderfolgenden Leal-Anweisungen in x86? .
Dem Compiler steht es auch frei, den umgebenden Code so zu transformieren, dass keine tatsächliche Verschiebung erfolgt oder er mit anderen Operationen kombiniert wird .
Zum Beispiel if(x<<1) { }
könnte ein verwendet werden and
, um alle Bits außer dem hohen Bit zu überprüfen. Auf x86 würden Sie eine test
Anweisung wie test eax, 0x7fffffff
/ jz .false
anstelle von verwenden shl eax,1 / jz
. Diese Optimierung funktioniert für jede Schichtanzahl und auch für Maschinen, bei denen große Schichten langsam (wie Pentium 4) oder nicht vorhanden (einige Mikrocontroller) sind.
Viele ISAs verfügen über Anweisungen zur Bitmanipulation, die über das reine Verschieben hinausgehen. zB PowerPC hat viele Anweisungen zum Extrahieren / Einfügen von Bitfeldern. Oder ARM hat Verschiebungen von Quelloperanden als Teil eines anderen Befehls. (Verschiebungs- / Drehanweisungen sind also nur eine spezielle Form der move
Verwendung einer verschobenen Quelle.)
Denken Sie daran, C ist keine Assemblersprache . Achten Sie immer auf die optimierte Compilerausgabe, wenn Sie Ihren Quellcode so optimieren , dass er effizient kompiliert.