[operators] 비트 시프트 (비트 시프트) 연산자 란 무엇이며 어떻게 작동합니까?

여가 시간에 C를 배우려고했지만 다른 언어 (C #, Java 등)는 동일한 개념 (그리고 종종 동일한 연산자)을 가지고 있습니다 …

핵심 수준에서, 내가 궁금하다, 무슨 않는 비트 이동 ( <<, >>, >>>) 수행이 해결 할 수있는 문제 및 개는이 벤드 주위에 숨어 무엇? 다시 말해, 모든 선의의 비트 이동에 대한 절대 초보자 안내서입니다.



답변

비트 시프 팅 연산자는 이름에서 의미하는 바를 정확하게 수행합니다. 그들은 비트를 이동시킵니다. 다음은 다양한 변속 연산자에 대한 간략한 소개입니다.

연산자

  • >> 산술 (또는 부호있는) 오른쪽 시프트 연산자입니다.
  • >>> 논리적 인 (또는 부호없는) 오른쪽 시프트 연산자입니다.
  • << 왼쪽 시프트 연산자이며 논리 및 산술 시프트의 요구를 모두 충족시킵니다.

이러한 연산자의 모든 정수 값에 적용 할 수 있습니다 ( int, long, 가능성 shortbytechar). 일부 언어에서는 시프트 연산자를 데이터 유형보다 작은 데이터 유형에 적용 int하면 피연산자의 크기 가 자동으로 조정됩니다 int.

참고 <<<가 중복 될 것이기 때문에, 운영자가 아닙니다.

또한 C와 C ++는 올바른 시프트 연산자를 구분하지 않습니다 . >>연산자 만 제공 하며 올바른 이동 동작은 서명 된 유형에 대해 정의 된 구현입니다. 나머지 답변은 C # / Java 연산자를 사용합니다.

(GCC와 연타 / LLVM을 포함한 모든 주류 C 및 C ++ 구현에서 >>. 서명 유형에 일부 코드는 다음을 가정 연산이지만, 뭔가 표준 보증을하지 않습니다 그것은 아닙니다. 정의되지 않은 ,하지만, 표준이 하나를 정의하는 구현을 필요로 그러나 음수 부호가있는 숫자의 왼쪽 이동 정의되지 않은 동작 (부호가있는 정수 오버플로)이므로 산술 오른쪽 이동이 필요하지 않은 경우 일반적으로 부호없는 유형으로 비트 이동을 수행하는 것이 좋습니다.)


왼쪽 시프트 (<<)

정수는 일련의 비트로 메모리에 저장됩니다. 예를 들어 32 비트로 저장된 숫자 6 int은 다음과 같습니다.

00000000 00000000 00000000 00000110

이 비트 패턴을 왼쪽으로 한 위치 ( 6 << 1)로 이동하면 숫자 12가됩니다.

00000000 00000000 00000000 00001100

보시다시피 숫자는 한 위치 씩 왼쪽으로 이동했으며 오른쪽의 마지막 숫자는 0으로 채워집니다. 또한 왼쪽으로 이동하여 2. 지금의 힘에 의해 곱셈에 해당주의 수도 6 << 1에 해당 6 * 2하고, 6 << 3동일합니다 6 * 8. 좋은 최적화 컴파일러는 가능하면 곱셈을 시프트로 대체합니다.

비 원형 변속

이들은 원형 교대 가 아닙니다 . 이 값을 왼쪽으로 한 위치 이동 ( 3,758,096,384 << 1) :

11100000 00000000 00000000 00000000

3,221,225,472의 결과 :

11000000 00000000 00000000 00000000

“끝에서 벗어난”숫자는 사라집니다. 감싸지 않습니다.


논리 오른쪽 시프트 (>>>)

논리적 오른쪽 시프트는 왼쪽 시프트와 반대입니다. 비트를 왼쪽으로 이동하지 않고 단순히 오른쪽으로 이동합니다. 예를 들어 숫자 12를 이동하면 :

00000000 00000000 00000000 00001100

오른쪽으로 한 위치 ( 12 >>> 1)만큼 원래 6을 다시 가져옵니다.

00000000 00000000 00000000 00000110

따라서 오른쪽으로 이동하는 것은 2의 거듭 제곱으로 나누는 것과 같습니다.

잃어버린 비트가 사라졌습니다

그러나 시프트는 “손실 된”비트를 회수 할 수 없습니다. 예를 들어,이 패턴을 이동 시키면 :

00111000 00000000 00000000 00000110

왼쪽의 4 개의 위치 ( 939,524,102 << 4)에 2,147,483,744를 얻습니다.

10000000 00000000 00000000 01100000

( (939,524,102 << 4) >>> 4) 뒤로 이동 하면 134,217,734가됩니다.

00001000 00000000 00000000 00000110

비트가 손실되면 원래 값으로 되돌릴 수 없습니다.


산술 오른쪽 시프트 (>>)

산술 오른쪽 이동은 논리 오른쪽 이동과 동일하지만 0으로 채워지는 대신 가장 중요한 비트로 채워집니다. 가장 중요한 비트가 부호 비트이거나 양수와 음수를 구별하는 비트 이기 때문 입니다. 가장 중요한 비트로 패딩함으로써 산술 오른쪽 이동은 부호 보존입니다.

예를 들어이 비트 패턴을 음수로 해석하면

10000000 00000000 00000000 01100000

숫자는 -2,147,483,552입니다. 산술 시프트 (-2,147,483,552 >> 4)로 이것을 오른쪽 4 위치로 옮기면 다음과 같이됩니다.

11111000 00000000 00000000 00000110

또는 숫자 -134,217,722입니다.

따라서 우리는 논리적 인 오른쪽 이동이 아닌 산술 오른쪽 이동을 사용하여 음수 부호를 보존했습니다. 그리고 다시 한번, 우리는 2의 거듭 제곱으로 나눗셈을하고 있음을 알 수 있습니다.


답변

단일 바이트가 있다고 가정 해 봅시다.

0110110

하나의 왼쪽 비트 시프트를 적용하면 다음과 같이됩니다.

1101100

가장 왼쪽에있는 0은 바이트에서 벗어나고 바이트의 오른쪽 끝에 새로운 0이 추가되었습니다.

비트는 롤오버되지 않습니다. 그들은 버려집니다. 즉, 1101100을 왼쪽으로 옮긴 다음 오른쪽으로 옮길 경우 동일한 결과를 얻지 못합니다.

왼쪽으로 N만큼 이동하면 2N 으로 곱하는 것과 같습니다 .

N만큼 오른쪽으로 이동하면 ( 1의 보수를 사용 하는 경우 )는 2N 으로 나누고 반올림 하는 것과 같습니다 .

비트 쉬프팅은 2의 거듭 제곱으로 작업하는 경우, 매우 빠른 곱셈과 나눗셈에 사용할 수 있습니다. 거의 모든 저수준 그래픽 루틴은 비트 쉬프팅을 사용합니다.

예를 들어 옛날에는 게임에 모드 13h (320×200 256 색)를 사용했습니다. 모드 13h에서, 비디오 메모리는 픽셀마다 순차적으로 배치된다. 즉, 픽셀의 위치를 ​​계산하기 위해 다음 수학을 사용합니다.

memoryOffset = (row * 320) + column

요즘 시대에는 속도가 중요했기 때문에 비트 시프트를 사용하여이 작업을 수행했습니다.

그러나 320은 2의 거듭 제곱이 아니므로이 문제를 해결하려면 함께 더한 2의 거듭 제곱이 320을 만드는 것을 찾아야합니다.

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

이제 왼쪽 시프트로 변환 할 수 있습니다.

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

최종 결과 :

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

고가의 곱셈 연산 대신 두 개의 비트 시프트를 사용한다는 점을 제외하고는 이전과 동일한 오프셋을 얻습니다. 몇 가지 실수와 32 비트 예제를 추가했습니다))

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

전체 : 고대 CPU가 이러한 타이밍을 가졌을 때마다 28주기.

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]

동일한 고대 CPU에서 12주기.

예, 우리는 16 번의 CPU 사이클을 줄이기 위해 열심히 노력할 것입니다.

32 또는 64 비트 모드에서 두 버전 모두 훨씬 짧아지고 빠릅니다. Intel Skylake ( http://agner.org/optimize/ 참조 ) 와 같은 최신 비 순차적 실행 CPU 는 매우 빠른 하드웨어 곱셈 (낮은 대기 시간 및 높은 처리량)을 가지므로 이득이 훨씬 작습니다. AMD 불도저 제품군은 특히 64 비트 곱하기에 약간 느립니다. Intel CPU 및 AMD Ryzen에서 두 가지 변화는 대기 시간이 약간 적지 만 곱하기보다 더 많은 명령어 수 (처리량 감소로 이어질 수 있음) :

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.

컴파일러는이를 위해 다음을 수행합니다 . 최적화 할 때 GCC, Clang 및 Microsoft Visual C ++가 모두 shift + lea를 사용return 320*row + col; 하는 방법을 확인하십시오 .

여기서 가장 주목할만한 점은 x86에는LEA 작은 왼쪽 시프트를 수행하고 동시에 명령어로 수행 할 수 있는 시프트 앤 추가 명령어 ( ) 가 있다는 것 add입니다. ARM은 더욱 강력합니다. 명령의 피연산자 하나를 왼쪽이나 오른쪽으로 자유롭게 이동할 수 있습니다. 따라서 2의 거듭 제곱으로 알려진 컴파일 시간 상수에 의한 스케일링은 곱하는 것보다 훨씬 효율적일 수 있습니다.


좋아, 현대에 … 이제 더 유용한 것은 비트 시프 팅을 사용하여 두 개의 8 비트 값을 16 비트 정수로 저장하는 것입니다. 예를 들어, C #에서 :

// Byte1: 11110000
// Byte2: 00001111

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

// value = 000011111110000;

C ++에서 struct두 개의 8 비트 멤버와 함께 a를 사용한 경우 컴파일러 에서이를 수행해야하지만 실제로는 항상 그렇지는 않습니다.


답변

비트 시프트를 포함한 비트 단위 연산은 저수준 하드웨어 또는 임베디드 프로그래밍의 기본입니다. 장치 또는 일부 이진 파일 형식에 대한 사양을 읽으면 바이트, 워드 및 dword가 다양한 관심 대상 값을 포함하는 바이트 단위로 정렬되지 않은 비트 필드로 분류 된 것을 볼 수 있습니다. 읽기 / 쓰기를 위해 이러한 비트 필드에 액세스하는 것이 가장 일반적인 사용법입니다.

그래픽 프로그래밍에서 간단한 실제 예는 16 비트 픽셀이 다음과 같이 표현된다는 것입니다.

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

녹색 가치를 얻으려면 다음과 같이하십시오.

 #define GREEN_MASK  0x7E0
 #define GREEN_OFFSET  5

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

설명

오프셋 5에서 시작하여 10 (즉, 6 비트 길이)으로 끝나는 녹색 만 값을 얻으려면 전체 16 비트 픽셀에 적용 할 때 (비트) 마스크를 사용해야합니다. 우리가 관심있는 비트 만.

#define GREEN_MASK  0x7E0

적절한 마스크는 0x7E0이며 이진수는 0000011111100000 (십진수 2016)입니다.

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

마스크를 적용하려면 AND 연산자 (&)를 사용하십시오.

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

마스크를 적용한 후 MSB가 11 비트에 있기 때문에 실제로는 11 비트 숫자 인 16 비트 숫자로 끝납니다. 녹색은 실제로 6 비트 길이이므로 오른쪽 이동 (11-6 = 5)을 사용하여 축소해야하므로 5를 오프셋 ( #define GREEN_OFFSET 5)으로 사용합니다.

2의 거듭 제곱으로 빠른 곱셈 및 나눗셈을 위해 비트 시프트를 사용하는 것도 일반적입니다.

 i <<= x;  // i *= 2^x;
 i >>= y;  // i /= 2^y;


답변

비트 마스킹 및 쉬프팅

비트 시프 팅은 종종 저수준 그래픽 프로그래밍에 사용됩니다. 예를 들어, 주어진 픽셀 색상 값은 32 비트 워드로 인코딩됩니다.

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

더 나은 이해를 위해 어떤 섹션으로 레이블이 지정된 동일한 이진 값은 어떤 색상 부분을 나타냅니다.

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

예를 들어이 픽셀 색상의 녹색 값을 얻고 싶다고 가정 해 봅시다. 우리는 마스킹시프트를 통해 그 가치를 쉽게 얻을 수 있습니다 .

우리의 마스크 :

                  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

논리 &연산자는 마스크가 1 인 값만 유지되도록합니다. 이제 마지막으로해야 할 일은 모든 비트를 오른쪽으로 16 자리 (논리적 오른쪽 이동) 만큼 이동하여 올바른 정수 값을 얻는 것 입니다.

 green_value = masked_color >>> 16

즉, 픽셀의 색상에서 녹색의 양을 나타내는 정수가 있습니다.

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

이는 종종 같은 부호화 또는 이미지 포맷을 디코딩하기 위해 사용되는 jpg, png등등


답변

한 가지 문제는 다음이 구현에 따라 다릅니다 (ANSI 표준에 따름).

char x = -1;
x >> 1;

x는 이제 127 (01111111) 또는 여전히 -1 (11111111) 일 수 있습니다.

실제로는 대개 후자입니다.


답변

팁과 요령 만 쓰고 있습니다. 시험 및 시험에 유용 할 수 있습니다.

  1. n = n*2: n = n<<1
  2. n = n/2: n = n>>1
  3. n이 2의 거듭 제곱인지 확인 (1,2,4,8, …) : 확인 !(n & (n-1))
  4. x 번째 비트 얻기 n:n |= (1 << x)
  5. x가 짝수인지 홀수인지 확인 : x&1 == 0(짝수)
  6. x 의 n 번째 비트를 토글합니다 .x ^ (1<<n)

답변

Java 구현에서 시프트 할 비트 수는 소스 크기에 따라 조정됩니다.

예를 들면 다음과 같습니다.

(long) 4 >> 65

비트를 오른쪽으로 65 번 이동하면 모든 것이 0이 될 것으로 예상 할 수 있지만 실제로는 다음과 같습니다.

(long) 4 >> (65 % 64)

이것은 <<, >> 및 >>>에 해당됩니다. 나는 다른 언어로 시도하지 않았습니다.