Czym są operatory bitowe przesunięcia (bit-shift) i jak działają?

Próbowałem nauczyć się C w wolnym czasie, i innych języków (C#, Java, itp.) mają tę samą koncepcję (i często te same operatory)...

Zastanawiam się, na poziomie podstawowym, co robi bit-shifting (>, >>>) czy, jakie problemy może pomóc rozwiązać, i co gotchas czają się za zakrętem? Innymi słowy, absolutny przewodnik dla początkujących do zmiany bitu w całej jego dobroci.

Author: Alexis King, 2008-09-26

8 answers

Operatory przesuwania bitów robią dokładnie to, co sugeruje ich nazwa. Przenoszą kawałki. Oto krótkie (lub nie tak krótkie) wprowadzenie do różnych operatorów zmiany.

Operatory

  • >> jest arytmetycznym (lub podpisanym) operatorem przesunięcia prawego.
  • >>> jest logicznym (lub niepodpisanym) operatorem przesunięcia prawego.
  • << jest operatorem zmiany lewej i spełnia potrzeby zarówno zmian logicznych, jak i arytmetycznych.

Wszystkie te operatory może być stosowany do wartości całkowitych(int, long, ewentualnie short i byte lub char). W niektórych językach zastosowanie operatorów przesunięcia do dowolnego typu danych mniejszego niż int automatycznie zmienia rozmiar operandu na int.

Zauważ, że <<< nie jest operatorem, ponieważ byłby zbędny. Zauważ również, że C i c++ nie rozróżniają właściwych operatorów zmiany. Dostarczają one tylko operatora >>, a zachowanie przesunięcia prawa jest implementacją zdefiniowaną dla signed typy.


Left shift (

Liczby całkowite są przechowywane w pamięci jako seria bitów. Na przykład liczba 6 zapisana jako 32-bitowa int to:

00000000 00000000 00000000 00000110

Przesunięcie tego wzorca bitowego w lewo o jedną pozycję (6 << 1) skutkowałoby liczbą 12:

00000000 00000000 00000000 00001100

Jak widać, cyfry zostały przesunięte w lewo o jedną pozycję, a ostatnia cyfra po prawej jest wypełniona zerem. Można również zauważyć, że przesunięcie w lewo jest równoznaczne z mnożeniem przez uprawnienia 2. Zatem {[24] } jest równoważne 6 * 2, a 6 << 3 jest równoważne 6 * 8. Dobry kompilator optymalizujący zastąpi mnożenia przesunięciami, gdy będzie to możliwe.

Przesunięcie Niekołowe

Proszę zauważyć, że są to , a nie przesunięcia kołowe. Przesunięcie tej wartości w lewo o jedną pozycję (3,758,096,384 << 1):

11100000 00000000 00000000 00000000

Wyniki w 3.221.225.472:

11000000 00000000 00000000 00000000

Cyfra przesunięta "poza koniec" jest tracona. Nie owija się w pobliżu.


Logiczne przesunięcie w prawo (>>>)

Logiczne przesunięcie w prawo to przesunięcie w lewo. Zamiast przesuwać bity w lewo, po prostu przesuwają się w prawo. Na przykład przesunięcie liczby 12:

00000000 00000000 00000000 00001100

W Prawo o jedną pozycję (12 >>> 1) odzyskamy naszą oryginalną 6:

00000000 00000000 00000000 00000110

Widzimy więc, że przesunięcie w prawo jest równoznaczne z podziałem przez potęgi 2.

Lost bits are gone

Jednak zmiana nie może odzyskać "zagubione" kawałki. Na przykład, jeśli przesuniemy ten wzór:

00111000 00000000 00000000 00000110

W lewo 4 pozycje (939,524,102 << 4), otrzymujemy 2,147,483,744:

10000000 00000000 00000000 01100000

A następnie cofając ((939,524,102 << 4) >>> 4) otrzymujemy 134.217.734:

00001000 00000000 00000000 00000110

Nie możemy odzyskać naszej pierwotnej wartości po utracie bitów.


Arytmetyczna przesunięcie w prawo ( > > )

Arytmetyczne przesunięcie w prawo jest dokładnie takie samo jak logiczne przesunięcie w prawo, z tym, że zamiast padding z zero, padding z najbardziej znaczącym bitem. To jest ponieważ najbardziej znaczącym bitem jest znak bit, czyli bit, który odróżnia liczby dodatnie od ujemnych. Przez wypełnienie najbardziej znaczącym bitem, arytmetyczne przesunięcie w prawo jest zachowujące znak.

Na przykład, jeśli interpretujemy ten wzór bitowy jako liczbę ujemną:

10000000 00000000 00000000 01100000

Mamy liczbę -2,147,483,552. Przesunięcie tej pozycji w prawo 4 z przesunięciem arytmetycznym (-2,147,483,552 >> 4) dałoby nam:

11111000 00000000 00000000 00000110

Lub numer -134,217,722.

Widzimy więc, że zachowaliśmy znak naszych liczb ujemnych, używając arytmetycznego przesunięcia w prawo, a nie logicznego przesunięcia w prawo. I po raz kolejny widzimy, że dokonujemy podziału przez potęgi 2.

 1529
Author: Derek Park,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-07 07:24:54

Powiedzmy, że mamy pojedynczy bajt:

0110110

Zastosowanie jednego lewego bitshift daje nam:

1101100

Lewe zero zostało przesunięte poza bajt, a nowe zero zostało dodane do prawego końca bajtu.

Bity nie przewracają się; są odrzucane. Oznacza to, że jeśli przesuniesz w lewo 1101100, a następnie przesuniesz w prawo, nie otrzymasz tego samego wyniku z powrotem.

Przesunięcie w lewo przez N jest równoważne mnożeniu przez 2 N .

Przesunięcie w prawo przez N jest (jeśli używasz dopełniacza jedynek ) jest odpowiednikiem dzielenia przez 2 N i zaokrąglania do zera.

Bitshifting może być używany do szalenie szybkiego mnożenia i dzielenia, pod warunkiem, że pracujesz z mocą 2. Prawie wszystkie niskopoziomowe procedury graficzne używają bitshiftingu.

Na przykład w dawnych czasach używaliśmy trybu 13h (320x200 256 kolorów) do gier. W trybie 13h pamięć wideo była rozkładana kolejno na piksel. To oznaczało obliczenie miejsce dla piksela, można użyć następującej matematyki:

memoryOffset = (row * 320) + column
W tamtych czasach prędkość była krytyczna, więc do tej operacji użyliśmy bitshiftów. Jednak 320 nie jest potęgą dwóch, więc aby to obejść, musimy dowiedzieć się, co jest potęgą dwóch, które razem tworzą 320: {16]}
(row * 320) = (row * 256) + (row * 64)

Teraz możemy zamienić to na przesunięcia w lewo:

(row * 320) = (row << 8) + (row << 6)

Dla wyniku końcowego:

memoryOffset = ((row << 8) + (row << 6)) + column

Teraz otrzymujemy to samo przesunięcie co poprzednio, z wyjątkiem zamiast kosztownej operacji mnożenia, używamy dwóch bitshifts...in x86 byłoby to coś takiego (Uwaga, minęło wieki odkąd zrobiłem assembly( Uwaga redaktora: poprawiłem kilka błędów i dodałem przykład 32-bitowy)): {]}

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

Razem: 28 cykli na dowolnym starożytnym procesorze.

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, tak ciężko pracowalibyśmy, aby zredukować 16 cykli procesora.

W trybie 32 lub 64-bitowym, obie wersje są znacznie krótsze i szybsze. Nowoczesne procesory out-of-order execution, takie jak Intel Skylake (zobacz http://agner.org/optimize / ) mają bardzo szybkie mnożenie sprzętu (niskie opóźnienia i wysoka przepustowość), więc zysk jest znacznie mniejszy. AMD Bulldozer-rodzina jest nieco wolniejsza, szczególnie dla 64-bitowych multiply. Na procesorach Intel i AMD Ryzen dwie zmiany to nieco mniejsze opóźnienie, ale więcej instrukcji niż mnożenie (co może prowadzić do niższego przepustowość): {]}

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 MSVC używają shift + lea podczas optymalizacji return 320*row + col;.

Najciekawsze jest to, że x86 ma instrukcję shift-and-add (LEA) może to robić małe zmiany w lewo i dodawać jednocześnie, z instrukcją performance as and add. Ramię jest jeszcze potężniejsze: jeden operand dowolnej instrukcji może być przesunięty w lewo lub w prawo za darmo. Więc skalowanie przez stałą czasu kompilacji, która jest znana jako moc 2, może być jeszcze bardziej wydajne niż mnożenie.


W dzisiejszych czasach... coś bardziej użytecznego teraz byłoby użycie bitshiftingu do przechowywania dwóch 8-bitowych wartości w 16-bitowej liczbie całkowitej. Na przykład w C#:
// Byte1: 11110000
// Byte2: 00001111

Int16 value = ((byte)(Byte1 >> 8) | Byte2));

// value = 000011111110000;

W C++ Kompilatory powinny zrobić to za Ciebie, jeśli używasz struct z dwoma składnikami 8-bitowymi, ale w praktyce nie zawsze.

 173
Author: FlySwat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-06-09 04:56:39

Operacje bitowe, w tym przesunięcie bitowe, są fundamentalne dla niskopoziomowego sprzętu lub programowania wbudowanego. Jeśli czytasz specyfikację urządzenia lub nawet niektórych formatów plików binarnych, zobaczysz bajty, słowa i dwords, podzielone na nie-bajtowe pola bitowe, które zawierają różne interesujące wartości. Dostęp do tych pól bitowych do odczytu/zapisu jest najczęściej używany.

Prostym prawdziwym przykładem w programowaniu Grafiki jest to, że 16-bitowy piksel jest reprezentowany jako follows:

  bit | 15| 14| 13| 12| 11| 10| 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1  | 0 |
      |       Blue        |         Green         |       Red          |

Aby uzyskać wartość green należy zrobić to:

 #define GREEN_MASK  0x7E0
 #define GREEN_OFFSET  5

 // Read green
 uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;

Wyjaśnienie

Aby uzyskać wartość green ONLY, która zaczyna się od przesunięcia 5 i kończy się na 10 (tj. długości 6 bitów), należy użyć maski (bitowej), która po nałożeniu na cały 16-bitowy piksel, uzyska tylko te bity, które nas interesują.

#define GREEN_MASK  0x7E0

Odpowiednią maską jest 0x7e0, która w układzie binarnym wynosi 000001111100000 (co w układzie dziesiętnym oznacza 2016).

uint16_t green = (pixel & GREEN_MASK) ...;

Aby zastosować Maska, używasz operatora AND ( & ).

uint16_t green = (pixel & GREEN_MASK) >> GREEN_OFFSET;

Po nałożeniu maski, skończysz z 16-bitową liczbą, która jest tak naprawdę tylko 11-bitową liczbą, ponieważ jej MSB jest w 11-tym bitie. Zielony ma długość tylko 6 bitów, więc musimy przeskalować go w dół, używając prawego przesunięcia (11-6 = 5), stąd użycie 5 jako przesunięcia (#define GREEN_OFFSET 5).

Popularne jest również używanie przesunięć bitowych do szybkiego mnożenia i dzielenia przez potęgi 2:

 i <<= x;  // i *= 2^x;
 i >>= y;  // i /= 2^y;
 86
Author: robottobor,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-08 18:11:10

Bit Masking & Shifting

Przesunięcie bitowe jest często używane w programowaniu Grafiki niskiego poziomu. Na przykład wartość koloru Piksela zakodowana w 32-bitowym słowie.
 Pixel-Color Value in Hex:    B9B9B900
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

Dla lepszego zrozumienia, ta sama wartość binarna oznaczona tym, które sekcje reprezentują jaką część koloru.

                                 Red     Green     Blue       Alpha
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

Powiedzmy na przykład, że chcemy uzyskać zieloną wartość tego koloru pikseli. Możemy łatwo uzyskać tę wartość przez maskowanie i przesuwanie.

Nasz Maska:

                  Red      Green      Blue      Alpha
 color :        10111001  10111001  10111001  00000000
 green_mask  :  00000000  11111111  00000000  00000000

 masked_color = color & green_mask

 masked_color:  00000000  10111001  00000000  00000000

Operator logiczny & zapewnia, że zachowywane są tylko wartości, w których Maska wynosi 1. Ostatnią rzeczą, jaką musimy teraz zrobić, jest uzyskanie poprawnej wartości całkowitej przez przesunięcie wszystkich tych bitów w prawo o 16 miejsc (logiczne przesunięcie w prawo).

 green_value = masked_color >>> 16

Et voilá, mamy liczbę całkowitą reprezentującą ilość zieleni w Kolorze pikseli:

 Pixels-Green Value in Hex:     000000B9
 Pixels-Green Value in Binary:  00000000 00000000 00000000 10111001 
 Pixels-Green Value in Decimal: 185

Jest to często używane do kodowania lub dekodowania formatów obrazu, takich jak jpg,png,....

 39
Author: Basti Funck,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-27 23:25:24

Jedną z nich jest to, że w zależności od implementacji (zgodnie ze standardem ANSI):

char x = -1;
x >> 1;

X może teraz wynosić 127 (01111111) lub nadal -1 (11111111).

W praktyce zwykle jest to drugie.

 26
Author: AShelly,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-27 00:56:51

Zauważ, że w implementacji Javy liczba bitów do przesunięcia jest modyfikowana przez rozmiar źródła.

Na przykład:

(long) 4 >> 65

Równa się 2. Można się spodziewać, że przesunięcie bitów w prawo 65 razy zerowałoby wszystko, ale w rzeczywistości jest to odpowiednik:

(long) 4 >> (65 % 64)

To prawda dla > i>>>. Nie próbowałem go w innych językach.

 8
Author: Patrick Monkelban,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-08-28 13:16:33

Piszę tylko porady i triki, mogą się przydać w testach/egzaminach.

  1. n = n*2: n = n<<1
  2. n = n/2: n = n>>1
  3. sprawdzanie, czy N jest potęgą 2 (1,2,4,8,...): check !(n & (n-1))
  4. Getting xth bit z n: n |= (1 << x)
  5. sprawdzanie czy x jest parzyste czy nieparzyste: x&1 == 0 (parzyste)
  6. Przełącz nth bit x: x ^ (1<<n)
 8
Author: Ravi Prakash,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-10 18:17:22

Należy pamiętać, że tylko 32-bitowa wersja PHP jest dostępna na platformie Windows.

Wtedy, jeśli na przykład przesuniesz > więcej niż o 31 bitów, wyniki są nieprzewidywalne. Zazwyczaj zwracana jest oryginalna liczba zamiast zer i może to być bardzo trudny błąd.

Oczywiście jeśli używasz 64-bitowej wersji PHP (unix), powinieneś unikać przesuwania o więcej niż 63 bity. Jednak na przykład MySQL używa 64-bitowego BIGINTA, więc nie powinno być żadnej kompatybilności problemy.

Aktualizacja: od PHP7 Windows, PHP buildy są w końcu w stanie używać pełnych 64-bitowych liczb całkowitych: wielkość liczby całkowitej jest zależna od platformy, chociaż maksymalna wartość około dwóch miliardów jest zwykłą wartością (to jest 32 bity podpisane). Platformy 64-bitowe zwykle mają maksymalną wartość około 9E18, z wyjątkiem Windows przed PHP 7, gdzie zawsze był 32 bit.

 -2
Author: lukyer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-26 20:42:49