What are bitwise shift (bit-shift) operators and h

2018-12-31 00:37发布

I've been attempting to learn C in my spare time, and other languages (C#, Java, etc.) have the same concept (and often the same operators) ...

What I'm wondering is, at a core level, what does bit-shifting (<<, >>, >>>) do, what problems can it help solve, and what gotchas lurk around the bend? In other words, an absolute beginner's guide to bit shifting in all its goodness.

8条回答
临风纵饮
2楼-- · 2018-12-31 00:44

Bitwise operations, including bit shift, are fundamental to low-level hardware or embedded programming. If you read a specification for a device or even some binary file formats, you will see bytes, words, and dwords, broken up into non-byte aligned bitfields, which contain various values of interest. Accessing these bit-fields for reading/writing is the most common usage.

A simple real example in graphics programming is that a 16-bit pixel is represented as follows:

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

To get at the green value you would do this:

 #define GREEN_MASK  0x7E0
 #define GREEN_OFFSET  5

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

Explanation

In order to obtain the value of green ONLY, which starts at offset 5 and ends at 10 (i.e. 6-bits long), you need to use a (bit) mask, which when applied against the entire 16-bit pixel, will yield only the bits we are interested in.

#define GREEN_MASK  0x7E0

The appropriate mask is 0x7E0 which in binary is 0000011111100000 (which is 2016 in decimal).

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

To apply a mask, you use the AND operator (&).

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

After applying the mask, you'll end up with a 16-bit number which is really just a 11-bit number since its MSB is in the 11th bit. Green is actually only 6-bits long, so we need to scale it down using a right shift (11 - 6 = 5), hence the use of 5 as offset (#define GREEN_OFFSET 5).

Also common is using bit shifts for fast multiplication and division by powers of 2:

 i <<= x;  // i *= 2^x;
 i >>= y;  // i /= 2^y;
查看更多
深知你不懂我心
3楼-- · 2018-12-31 00:44

Be aware of that only 32 bit version of PHP is available on the Windows platform.

Then if you for instance shift << or >> more than by 31 bits, results are unexpectable. Usually the original number instead of zeros will be returned, and it can be a really tricky bug.

Of course if you use 64 bit version of PHP (unix), you should avoid shifting by more than 63 bits. However, for instance, MySQL uses the 64-bit BIGINT, so there should not be any compatibility problems.

UPDATE: From PHP7 Windows, php builds are finally able to use full 64bit integers: The size of an integer is platform-dependent, although a maximum value of about two billion is the usual value (that's 32 bits signed). 64-bit platforms usually have a maximum value of about 9E18, except on Windows prior to PHP 7, where it was always 32 bit.

查看更多
永恒的永恒
4楼-- · 2018-12-31 00:55

Bit Masking & Shifting

Bit shifting is often used in low level graphics programming. For example a given pixel color value encoded in a 32-bit word.

 Pixel-Color Value in Hex:    B9B9B900
 Pixel-Color Value in Binary: 10111001  10111001  10111001  00000000

For better understanding, the same binary value labeled with what sections represents what color part.

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

Let's say for example we want to get the green value of this pixels color. We can easily get that value by masking and shifting.

Our mask:

                  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

The logical & operator ensures that only the values where the mask is 1 are kept. The last thing we now have to do, is to get the correct integer value by shifting all those bits to the right by 16 places (logical right shift).

 green_value = masked_color >>> 16

Et voilá, we have the integer representing the amount of green in the pixels color:

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

This is often used for encoding or decoding image formats like jpg,png,....

查看更多
谁念西风独自凉
5楼-- · 2018-12-31 00:55

One gotcha is that the following is implementation dependent (according to the ANSI standard):

char x = -1;
x >> 1;

x can now be 127 (01111111) or still -1 (11111111).

In practice, it's usually the latter.

查看更多
深知你不懂我心
6楼-- · 2018-12-31 00:56

Note that in the Java implementation, the number of bits to shift is mod'd by the size of the source.

For example:

(long) 4 >> 65

equals 2. You might expect shifting the bits to the right 65 times would zero everything out, but it's actually the equivalent of:

(long) 4 >> (65 % 64)

This is true for <<, >>, and >>>. I have not tried it out in other languages.

查看更多
伤终究还是伤i
7楼-- · 2018-12-31 00:59

Let's say we have a single byte:

0110110

Applying a single left bitshift gets us:

1101100

The leftmost zero was shifted out of the byte, and a new zero was appended to the right end of the byte.

The bits don't rollover; they are discarded. That means if you left shift 1101100 and then right shift it, you won't get the same result back.

Shifting left by N is equivalent to multiplying by 2N.

Shifting right by N is (if you are using ones' complement) is the equivalent of dividing by 2N and rounding to zero.

Bitshifting can be used for insanely fast multiplication and division, provided you are working with a power of 2. Almost all low-level graphics routines use bitshifting.

For example, way back in the olden days, we used mode 13h (320x200 256 colors) for games. In Mode 13h, the video memory was laid out sequentially per pixel. That meant to calculate the location for a pixel, you would use the following math:

memoryOffset = (row * 320) + column

Now, back in that day and age, speed was critical, so we would use bitshifts to do this operation.

However, 320 is not a power of two, so to get around this we have to find out what is a power of two that added together makes 320:

(row * 320) = (row * 256) + (row * 64)

Now we can convert that into left shifts:

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

For a final result of:

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

Now we get the same offset as before, except instead of an expensive multiplication operation, we use the two bitshifts...in x86 it would be something like this (note, it's been forever since I've done assembly (editor's note: corrected a couple mistakes and added a 32-bit example)):

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

Total: 28 cycles on whatever ancient CPU had these timings.

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 cycles on the same ancient CPU.

Yes, we would work this hard to shave off 16 CPU cycles.

In 32 or 64-bit mode, both versions get a lot shorter and faster. Modern out-of-order execution CPUs like Intel Skylake (see http://agner.org/optimize/) have very fast hardware multiply (low latency and high throughput), so the gain is much smaller. AMD Bulldozer-family is a bit slower, especially for 64-bit multiply. On Intel CPUs, and AMD Ryzen, two shifts are slightly lower latency but more instructions than a multiply (which may lead to lower throughput):

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.

Compilers will do this for you: See how gcc, clang, and MSVC all use shift+lea when optimizing return 320*row + col;.

The most interesting thing to note here is that x86 has a shift-and-add instruction (LEA) that can do small left shifts and add at the same time, with the performance as and add instruction. ARM is even more powerful: one operand of any instruction can be left or right shifted for free. So scaling by a compile-time-constant that's known to be a power-of-2 can be even more efficient than a multiply.


OK, back in the modern days... something more useful now would be to use bitshifting to store two 8-bit values in a 16-bit integer. For example, in C#:

// Byte1: 11110000
// Byte2: 00001111

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

// value = 000011111110000;

In C++, compilers should do this for you if you used a struct with two 8-bit members, but in practice don't always.

查看更多
登录 后发表回答