Jak zawsze, zależy to od kontekstu otaczającego kodu : np. Czy używasz x<<1
jako indeksu tablicy? Lub dodać to do czegoś innego? W obu przypadkach małe liczby przesunięć (1 lub 2) mogą często zoptymalizować nawet bardziej, niż gdyby kompilator musiał po prostu przesunąć. Nie wspominając już o kompromisie między przepustowością a opóźnieniami a wąskimi gardłami front-endu. Wykonanie maleńkiego fragmentu nie jest jednowymiarowe.
Instrukcje zmiany sprzętu nie są jedyną opcją kompilatora podczas kompilacji x<<1
, ale inne odpowiedzi w większości zakładają to.
x << 1
jest dokładnie odpowiednikiemx+x
for unsigned i dla liczb całkowitych z dopełnieniem ze znakiem uzupełniającym. Kompilatory zawsze wiedzą, na jaki sprzęt są przeznaczone, podczas kompilacji, więc mogą wykorzystać takie sztuczki.
Na Intel Haswell , add
ma przepustowość 4 na zegar, ale shl
z natychmiastowym liczyć ma tylko 2 na przepustowość zegara. (Zobacz http://agner.org/optimize/, aby uzyskać tabele instrukcji i inne linki wx86tag wiki). Przesunięcia wektorów SIMD wynoszą 1 na zegar (2 w Skylake), ale sumy całkowitych wektorów SIMD to 2 na zegar (3 w Skylake). Jednak opóźnienie jest takie samo: 1 cykl.
Istnieje również specjalne kodowanie z przesunięciem o jeden, shl
gdzie liczba jest niejawna w kodzie operacyjnym. 8086 nie miało natychmiastowych zmian liczenia, tylko o jeden i według cl
rejestru. Jest to szczególnie istotne w przypadku przesunięć w prawo, ponieważ możesz po prostu dodać zmiany w lewo, chyba że przesuwasz operand pamięci. Ale jeśli wartość będzie potrzebna później, lepiej najpierw załadować do rejestru. Ale w każdym razie, shl eax,1
lub add eax,eax
jest o jeden bajt krótszy niż shl eax,10
, a rozmiar kodu może bezpośrednio (dekodować / wąskie gardła front-endu) lub pośrednio (chybienia w pamięci podręcznej kodu L1I) wpływać na wydajność.
Mówiąc bardziej ogólnie, małe liczby przesunięć można czasami zoptymalizować do skalowanego indeksu w trybie adresowania na platformie x86. Większość innych powszechnie używanych obecnie architektur to RISC i nie ma trybów adresowania ze skalowanymi indeksami, ale architektura x86 jest na tyle powszechna, że warto o tym wspomnieć. (jajko, jeśli indeksujesz tablicę elementów 4-bajtowych, jest miejsce na zwiększenie współczynnika skalowania o 1 int arr[]; arr[x<<1]
).
Konieczność kopiowania + przesunięcia jest powszechna w sytuacjach, w których x
nadal potrzebna jest pierwotna wartość . Ale większość instrukcji całkowitych x86 działa w miejscu. (Miejsce docelowe jest jednym ze źródeł instrukcji takich jak add
lub shl
.) Konwencja wywoływania Systemu V x86-64 przekazuje argumenty do rejestrów, przy czym pierwszy argument wchodzi edi
i zwraca wartość w eax
, więc funkcja, która zwraca, powoduje x<<10
również, że kompilator emituje copy + shift kod.
LEA
Instrukcja pozwala shift-and-add (o liczbie zmianowym od 0 do 3, ponieważ używa trybu adresowania maszynowy kodowanie). Wynik umieszcza w oddzielnym rejestrze.
gcc i clang optymalizują te funkcje w ten sam sposób, co widać w eksploratorze kompilatora Godbolt :
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 z 2 komponentami ma opóźnienie 1 cyklu i przepustowość 2 na takt w najnowszych procesorach Intel i AMD. (Rodzina Sandybridge i Bulldozer / Ryzen). W przypadku Intela jest to tylko 1 przepustowość na zegar z opóźnieniem 3c dla lea eax, [rdi + rsi + 123]
. (Powiązane: Dlaczego ten kod C ++ jest szybszy niż mój odręczny zestaw do testowania hipotezy Collatza? Omawiamy to szczegółowo).
W każdym razie kopiowanie + przesunięcie o 10 wymaga osobnej mov
instrukcji. Może to być zerowe opóźnienie na wielu ostatnich procesorach, ale nadal wymaga przepustowości front-endu i rozmiaru kodu. ( Czy plik MOV x86 naprawdę może być „darmowy”? Dlaczego w ogóle nie mogę tego odtworzyć? )
Również powiązane: Jak pomnożyć rejestr przez 37, używając tylko 2 kolejnych instrukcji leal w x86? .
Kompilator może również przekształcić otaczający kod, więc nie ma rzeczywistego przesunięcia lub jest połączony z innymi operacjami .
Na przykład if(x<<1) { }
mógłby użyć and
do sprawdzenia wszystkich bitów oprócz wysokiego bitu. Na x86 użyłbyś test
instrukcji, takiej jak test eax, 0x7fffffff
/ jz .false
zamiast shl eax,1 / jz
. Ta optymalizacja działa dla dowolnej liczby zmian, a także działa na maszynach, na których zmiany z dużą liczbą są powolne (jak Pentium 4) lub nie istnieją (niektóre mikrokontrolery).
Wiele ISA ma instrukcje dotyczące manipulacji bitami poza zwykłym przesunięciem. np. PowerPC ma wiele instrukcji wyodrębniania / wstawiania pól bitowych. Lub ARM ma przesunięcia argumentów źródłowych jako część dowolnej innej instrukcji. (Tak więc instrukcje przesuwania / obracania są tylko specjalną formą move
, używającą przesuniętego źródła).
Pamiętaj, C nie jest językiem asemblera . Zawsze patrz na zoptymalizowane dane wyjściowe kompilatora, gdy dostrajasz kod źródłowy w celu wydajnej kompilacji.