Powiedzmy, że mamy jeden bajt:
0110110
Zastosowanie pojedynczego przesunięcia bitów w lewo daje nam:
1101100
Lewe zero zostało przesunięte poza bajt, a nowe zero zostało dodane do prawego końca bajtu.
Bity nie przechodzą; są odrzucane. Oznacza to, że jeśli opuścisz Shift 1101100, a następnie prawy Shift, nie otrzymasz z powrotem tego samego wyniku.
Przesunięcie w lewo o N odpowiada przemnożeniu przez 2 N. .
Przesunięcie w prawo o N to (jeśli używasz ich uzupełnienia ) jest równoważne podzieleniu przez 2 N. i zaokrągleniu do zera.
Bitshifting może być użyty do niesamowicie szybkiego mnożenia i dzielenia, pod warunkiem, że pracujesz z potęgą 2. Prawie wszystkie procedury graficzne niskiego poziomu używają bitshiftingu.
Na przykład w dawnych czasach używaliśmy trybu 13h (320 x 200 256 kolorów) do gier. W trybie 13h pamięć wideo była układana sekwencyjnie na piksel. Oznaczało to obliczenie lokalizacji piksela, użyłbyś następującej matematyki:
memoryOffset = (row * 320) + column
W tamtych czasach prędkość była kluczowa, więc do wykonania tej operacji użyjemy przesunięć bitów.
Jednak 320 nie jest potęgą dwóch, więc aby to obejść, musimy dowiedzieć się, jaka potęga dwóch, dodanych razem, daje 320:
(row * 320) = (row * 256) + (row * 64)
Teraz możemy przekonwertować to na przesunięcia w lewo:
(row * 320) = (row << 8) + (row << 6)
Aby uzyskać końcowy wynik:
memoryOffset = ((row << 8) + (row << 6)) + column
Teraz otrzymujemy takie samo przesunięcie jak poprzednio, z tą różnicą, że zamiast kosztownej operacji mnożenia używamy dwóch przesunięć bitowych ... w x86 byłoby to mniej więcej tak (uwaga, to trwało wiecznie, odkąd zrobiłem montaż (uwaga redaktora: poprawiona kilka błędów i dodany 32-bitowy przykład)):
mov ax, 320; 2 cycles
mul word [row]; 22 CPU Cycles
mov di,ax; 2 cycles
add di, [column]; 2 cycles
; di = [row]*320 + [column]
; 16-bit addressing mode limitations:
; [di] is a valid addressing mode, but [ax] isn't, otherwise we could skip the last mov
Łącznie: 28 cykli na dowolnym starożytnym procesorze miał te czasy.
Vrs
mov ax, [row]; 2 cycles
mov di, ax; 2
shl ax, 6; 2
shl di, 8; 2
add di, ax; 2 (320 = 256+64)
add di, [column]; 2
; di = [row]*(256+64) + [column]
12 cykli na tym samym starożytnym procesorze.
Tak, ciężko pracowalibyśmy, aby zmniejszyć 16 cykli procesora.
W trybie 32- lub 64-bitowym obie wersje stają się znacznie krótsze i szybsze. Nowoczesne procesory poza kolejnością, takie jak Intel Skylake (patrz http://agner.org/optimize/ ), mają bardzo szybki mnożnik sprzętowy (małe opóźnienia i wysoka przepustowość), więc zysk jest znacznie mniejszy. Rodzina buldożerów AMD jest nieco wolniejsza, szczególnie w przypadku mnożenia 64-bitowego. W procesorach Intel i AMD Ryzen dwie zmiany mają nieco mniejsze opóźnienie, ale więcej instrukcji niż wielokrotność (co może prowadzić do niższej przepustowości):
imul edi, [row], 320 ; 3 cycle latency from [row] being ready
add edi, [column] ; 1 cycle latency (from [column] and edi being ready).
; edi = [row]*(256+64) + [column], in 4 cycles from [row] being ready.
vs.
mov edi, [row]
shl edi, 6 ; row*64. 1 cycle latency
lea edi, [edi + edi*4] ; row*(64 + 64*4). 1 cycle latency
add edi, [column] ; 1 cycle latency from edi and [column] both being ready
; edi = [row]*(256+64) + [column], in 3 cycles from [row] being ready.
Kompilatory zrobią to za Ciebie: Zobacz, jak GCC, Clang i Microsoft Visual C ++ używają shift + lea podczas optymalizacjireturn 320*row + col;
.
Najciekawszą rzeczą do odnotowania tutaj jest to, że x86 ma instrukcję shift-and-add ( LEA
), która może wykonywać małe lewe przesunięcia i dodawać w tym samym czasie, z wydajnością jako add
instrukcją. ARM jest jeszcze potężniejszy: jeden operand dowolnej instrukcji można przesunąć w lewo lub w prawo za darmo. Skalowanie według stałej czasowej kompilacji, o której wiadomo, że jest potęgą 2, może być nawet bardziej wydajne niż mnożenie.
OK, wracając do czasów współczesnych ... bardziej użyteczne byłoby użycie przesunięcia bitów do przechowywania dwóch 8-bitowych wartości w 16-bitowej liczbie całkowitej. Na przykład w języku C #:
// Byte1: 11110000
// Byte2: 00001111
Int16 value = ((byte)(Byte1 >> 8) | Byte2));
// value = 000011111110000;
W C ++ kompilatory powinny zrobić to za Ciebie, jeśli korzystasz struct
z dwóch 8-bitowych elementów, ale w praktyce nie zawsze.