나는 여가 시간에 C를 배우려고 시도했고 다른 언어(C#, Java 등)는 동일한 개념(그리고 종종 동일한 연산자)을 가지고 있습니다 ...
내가 궁금한 것은 핵심 수준에서 비트 시프팅( <<
, >>
, >>>
)은 무엇을 하는지, 어떤 문제를 해결하는 데 도움이 되며, 커브 주변에 어떤 문제가 숨어 있는지입니다. 다시 말해, 비트 시프팅의 모든 장점에 대한 절대적인 초보자 가이드입니다.
질문자 :John Rudy
나는 여가 시간에 C를 배우려고 시도했고 다른 언어(C#, Java 등)는 동일한 개념(그리고 종종 동일한 연산자)을 가지고 있습니다 ...
내가 궁금한 것은 핵심 수준에서 비트 시프팅( <<
, >>
, >>>
)은 무엇을 하는지, 어떤 문제를 해결하는 데 도움이 되며, 커브 주변에 어떤 문제가 숨어 있는지입니다. 다시 말해, 비트 시프팅의 모든 장점에 대한 절대적인 초보자 가이드입니다.
비트 시프팅 연산자는 이름이 의미하는 바를 정확히 수행합니다. 비트를 이동합니다. 다음은 다양한 시프트 연산자에 대한 간략한(또는 간략하지 않은) 소개입니다.
>>
는 산술(또는 부호 있는) 오른쪽 시프트 연산자입니다.>>>
는 논리(또는 부호 없는) 오른쪽 시프트 연산자입니다.<<
는 왼쪽 시프트 연산자이며 논리 및 산술 시프트의 요구 사항을 모두 충족합니다. 이러한 모든 연산자는 정수 값( int
, long
, short
및 byte
또는 char
)에 적용할 수 있습니다. 일부 언어의 경우,보다 작은 어떤 데이터 형 시프트 연산자를 적용 int
자동으로 피연산자의 크기를 조정 int
.
<<<
는 중복되므로 연산자가 아닙니다.
또한 C와 C++는 오른쪽 시프트 연산자를 구분하지 않습니다 . >>
연산자만 제공하며 오른쪽 이동 동작은 서명된 유형에 대해 정의된 구현입니다. 나머지 답변은 C#/Java 연산자를 사용합니다.
(GCC 및 Clang/LLVM을 포함한 모든 주류 C 및 C++ 구현 >>
는 산술적입니다. 일부 코드에서는 이를 가정하지만 표준이 보장하는 것은 아닙니다. 하지만 정의 되지 않은 것은 아닙니다. 표준에서는 구현에서 정의할 것을 요구합니다. 그러나 음수 부호의 왼쪽 이동 은 정의되지 않은 동작입니다(부호 있는 정수 오버플로).따라서 산술 오른쪽 이동이 필요하지 않는 한 일반적으로 부호 없는 유형으로 비트 이동을 수행하는 것이 좋습니다.
정수는 메모리에 일련의 비트로 저장됩니다. 예를 들어 32비트 int
로 저장된 숫자 6은 다음과 같습니다.
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만큼 왼쪽으로 이동하여 2 N 곱한 것과 같다.
N만큼 오른쪽으로 이동하는 것은 ( 1의 보수를 사용하는 경우) 2 N 으로 나누고 0으로 반올림하는 것과 같습니다.
비트시프팅은 2의 거듭제곱으로 작업하는 경우 엄청나게 빠른 곱셈과 나눗셈에 사용할 수 있습니다. 거의 모든 저수준 그래픽 루틴은 비트시프팅을 사용합니다.
예를 들어 옛날에는 게임에 모드 13h(320x200 256색)를 사용했습니다. 모드 13h에서 비디오 메모리는 픽셀당 순차적으로 배치되었습니다. 픽셀의 위치를 계산하려면 다음 수학을 사용합니다.
memoryOffset = (row * 320) + column
이제 그 당시에는 속도가 매우 중요했기 때문에 비트시프트를 사용하여 이 작업을 수행했습니다.
그러나 320은 2의 거듭제곱이 아니므로 이 문제를 해결하려면 함께 더하면 320이 되는 2의 거듭제곱이 무엇인지 찾아야 합니다.
(row * 320) = (row * 256) + (row * 64)
이제 이를 왼쪽 시프트로 변환할 수 있습니다.
(row * 320) = (row << 8) + (row << 6)
최종 결과:
memoryOffset = ((row << 8) + (row << 6)) + column
이제 우리는 값비싼 곱셈 연산 대신에 두 개의 비트시프트를 사용한다는 점을 제외하고는 이전과 동일한 오프셋을 얻습니다. x86에서는 다음과 같을 것입니다. 몇 가지 실수와 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주기.
대
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 Bulldozer 제품군은 특히 64비트 곱셈의 경우 약간 느립니다. Intel CPU 및 AMD Ryzen에서 2개의 시프트는 대기 시간이 약간 낮지만 곱하기보다 명령이 더 많습니다(처리량이 낮아질 수 있음).
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.
대
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.
컴파일러가 다음을 수행합니다. return 320*row + col;
최적화할 때 GCC, Clang 및 Microsoft Visual C++가 모두 shift+lea를 사용하는 방법을 확인하십시오. .
여기서 주목해야 할 가장 흥미로운 점은 x86에는 작은 왼쪽 시프트와 더하기를 동시에 수행할 수 있는 시프트 및 더하기 명령어( LEA
) add
명령어로서의 성능이 있다는 것입니다. ARM은 훨씬 더 강력합니다. 모든 명령어의 피연산자 하나를 왼쪽 또는 오른쪽으로 자유롭게 이동할 수 있습니다. 따라서 2의 거듭제곱으로 알려진 컴파일 시간 상수로 확장하는 것이 곱하기보다 훨씬 더 효율적일 수 있습니다.
좋아, 현대로 돌아가면... 비트시프팅을 사용하여 16비트 정수에 두 개의 8비트 값을 저장하는 것이 더 유용할 것입니다. 예를 들어 C#에서:
// Byte1: 11110000 // Byte2: 00001111 Int16 value = ((byte)(Byte1 >> 8) | Byte2)); // value = 000011111110000;
struct
를 사용한 경우 컴파일러에서 이 작업을 수행해야 하지만 실제로는 항상 그런 것은 아닙니다.
비트 시프트를 포함한 비트 연산은 저수준 하드웨어 또는 임베디드 프로그래밍의 기본입니다. 장치 또는 일부 이진 파일 형식에 대한 사양을 읽으면 바이트, 단어 및 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)이 될 수 있습니다.
실제로는 일반적으로 후자입니다.
나는 단지 팁과 트릭을 씁니다. 시험 및 시험에 유용할 수 있습니다.
n = n*2
: n = n<<1
n = n/2
: n = n>>1
!(n & (n-1))
n
x 번째 비트 얻기 : n |= (1 << x)
x&1 == 0
(짝수)x ^ (1<<n)
Java 구현에서 이동할 비트 수는 소스 크기에 따라 수정됩니다.
예를 들어:
(long) 4 >> 65
2와 같습니다. 비트를 오른쪽으로 65번 이동하면 모든 것이 0이 될 것으로 예상할 수 있지만 실제로는 다음과 같습니다.
(long) 4 >> (65 % 64)
이는 <<, >> 및 >>>에 해당됩니다. 다른 언어로는 시도하지 않았습니다.
비트 연산자는 비트 수준 연산을 수행하거나 다른 방식으로 비트를 조작하는 데 사용됩니다. 비트 연산은 훨씬 빠른 것으로 밝혀졌으며 프로그램의 효율성을 향상시키는 데 사용되기도 합니다. 기본적으로 Bitwise 연산자는 정수 유형인 long , int , short , char 및 byte 에 적용될 수 있습니다.
그들은 왼쪽 시프트와 오른쪽 시프트의 두 가지 범주로 분류됩니다.
출력: 6 , 여기서 3의 이진 표현은 0...0011(32비트 시스템 고려)이므로 한 번 이동하면 선행 0이 무시/손실되고 나머지 31비트는 모두 왼쪽으로 이동합니다. 그리고 끝에 0이 추가됩니다. 그래서 그것은 0...0110이 되었고, 이 숫자의 십진법 표현은 6입니다.
출력: -2 , Java 음수에서 2의 보수로 표시됩니다. SO, -1은 1....11(32비트 시스템 고려)과 동일한 2^32-1로 나타냅니다. 한 번 이동하면 선행 비트가 무시/손실되고 나머지 31비트는 왼쪽으로 이동하고 마지막에 0이 추가됩니다. 따라서 11...10이 되고 이에 해당하는 십진수는 -2입니다. 그래서, 나는 당신이 왼쪽 교대와 그 작동 방식에 대해 충분한 지식을 가지고 있다고 생각합니다.
출력: 8 , 32비트 시스템에서 35의 이진 표현은 00...00100011이므로 두 번 오른쪽 시프트하면 처음 30개의 선행 비트가 오른쪽으로 이동/이동하고 두 개의 하위 비트가 오른쪽으로 이동합니다. 비트는 손실/무시되고 선행 비트에 두 개의 0이 추가됩니다. 따라서 00....00001000이 됩니다. 이 이진 표현에 해당하는 십진수는 8입니다. 또는 다음 코드의 출력을 찾는 간단한 수학적 트릭 이 있습니다. 이를 일반화하려면 x >> y라고 말할 수 있습니다. = 바닥(x/pow(2,y)). 위의 예를 고려하면 x=35이고 y=2이므로 35/2^2 = 8.75이고 하한값을 취하면 답은 8입니다.
산출:
그러나 이 트릭은 y의 큰 값을 취하면 잘못된 출력을 제공하는 작은 y 값에 대해 괜찮습니다.
출력: -5 , 위에서 설명했듯이 컴파일러는 음수 값을 2의 보수로 저장합니다. 따라서 -10은 2^32-10으로 표현되며 32비트 시스템 11....0110을 고려하여 바이너리 표현으로 표현됩니다. 한 번 이동/이동할 때 처음 31개의 선행 비트가 오른쪽으로 이동하고 하위 비트가 손실/무시됩니다. 따라서 11...0011이 되고 이 숫자의 10진수 표현은 -5입니다(숫자의 부호를 어떻게 알 수 있습니까? 선행 비트가 1이기 때문에). -1을 오른쪽으로 이동하면 부호 확장이 상위 비트에서 계속 더 많은 것을 가져오기 때문에 결과가 항상 -1로 유지된다는 점에 주목하는 것이 흥미로웠습니다.
출력: 2147483647 , -2는 32비트 시스템에서 11...10으로 표시되기 때문입니다. 비트를 1만큼 이동하면 첫 번째 31개의 선행 비트가 오른쪽으로 이동/이동되고 하위 비트가 손실/무시되고 선행 비트에 0이 추가됩니다. 따라서 011...1111(2^31-1)이 되고 10진수 값은 2147483647이 됩니다.
Python의 몇 가지 유용한 비트 연산/조작.
Python에서 Ravi Prakash의 답변 을 구현했습니다.
# Basic bit operations # Integer to binary print(bin(10)) # Binary to integer print(int('1010', 2)) # Multiplying x with 2 .... x**2 == x << 1 print(200 << 1) # Dividing x with 2 .... x/2 == x >> 1 print(200 >> 1) # Modulo x with 2 .... x % 2 == x & 1 if 20 & 1 == 0: print("20 is a even number") # Check if n is power of 2: check !(n & (n-1)) print(not(33 & (33-1))) # Getting xth bit of n: (n >> x) & 1 print((10 >> 2) & 1) # Bin of 10 == 1010 and second bit is 0 # Toggle nth bit of x : x^(1 << n) # take bin(10) == 1010 and toggling second bit in bin(10) we get 1110 === bin(14) print(10^(1 << 2))
비트 시프트 연산자는 이진 개체의 비트 값을 이동합니다. 왼쪽 피연산자는 이동할 값을 지정합니다. 오른쪽 피연산자는 값의 비트가 이동될 위치의 수를 지정합니다. 결과는 lvalue가 아닙니다. 두 피연산자는 동일한 우선 순위를 가지며 왼쪽에서 오른쪽으로 연관됩니다.
Operator Usage << Indicates the bits are to be shifted to the left. >> Indicates the bits are to be shifted to the right.
각 피연산자는 정수 또는 열거형 형식을 가져야 합니다. 컴파일러는 피연산자에 대해 적분 승격을 수행한 다음 오른쪽 피연산자가 int 유형으로 변환됩니다. 결과는 왼쪽 피연산자와 동일한 유형을 갖습니다(산술 변환 후).
오른쪽 피연산자는 음수 값이나 이동되는 표현식의 너비(비트)보다 크거나 같은 값을 가질 수 없습니다. 이러한 값에 대한 비트 시프트의 결과는 예측할 수 없습니다.
오른쪽 피연산자의 값이 0이면 결과는 왼쪽 피연산자의 값입니다(일반적인 산술 변환 후).
<< 연산자는 비어 있는 비트를 0으로 채웁니다. 예를 들어, left_op의 값이 4019인 경우 left_op의 비트 패턴(16비트 형식)은 다음과 같습니다.
0000111110110011
left_op << 3 표현식은 다음을 생성합니다.
0111110110011000
left_op >> 3 표현식은 다음을 생성합니다.
0000000111110110
Windows 플랫폼에서는 32비트 버전의 PHP만 사용할 수 있습니다.
그런 다음 예를 들어 << 또는 >>를 31비트 이상 이동하면 예기치 않은 결과가 나타납니다. 일반적으로 0 대신 원래 숫자가 반환되며 이는 정말 까다로운 버그일 수 있습니다.
물론 64비트 버전의 PHP(Unix)를 사용한다면 63비트 이상 쉬프트를 피해야 합니다. 그러나 예를 들어 MySQL은 64비트 BIGINT를 사용하므로 호환성 문제가 없어야 합니다.
업데이트: PHP 7 Windows에서 PHP 빌드는 마침내 완전한 64비트 정수를 사용할 수 있게 되었습니다. 정수 의 크기는 플랫폼에 따라 다르지만 최대 약 20억의 값이 일반적인 값(32비트 부호 있음)입니다. 64비트 플랫폼은 항상 32비트였던 PHP 7 이전의 Windows를 제외하고 일반적으로 최대값이 약 9E18입니다.
출처 : http:www.stackoverflow.com/questions/141525/what-are-bitwise-shift-bit-shift-operators-and-how-do-they-work
치명적인 오류: Python.h: 해당 파일 또는 디렉터리가 없습니다. (0) | 2022.03.17 |
---|---|
스크롤 막대를 숨기지만 여전히 스크롤할 수 있습니다. (0) | 2022.03.17 |
모든 프로그래머가 읽어야 할 가장 영향력 있는 책은 무엇입니까? [닫은] (0) | 2022.03.17 |
컨테이너 너비에 따른 글꼴 크기 조정 (0) | 2022.03.17 |
SQL은 열에 최대 값이 있는 행만 선택합니다. (0) | 2022.03.17 |