🔥 비트 연산자

1202자
15분

비트 연산자(Bitwise Operators)는 데이터 구조 내의 개별 원시 데이터 비트를 조작할 수 있게 해줍니다. 그래픽 프로그래밍이나 디바이스 드라이버 생성과 같은 저수준 프로그래밍에서 자주 사용되곤 하지요. 또한 사용자 정의 프로토콜을 통해 통신하기 위한 데이터 인코딩 및 디코딩과 같이 외부 소스의 원시 데이터를 다룰 때에도 비트 연산자가 유용할 수 있답니다.

Swift는 아래에 설명된 바와 같이 C에서 발견되는 모든 비트 연산자를 지원하고 있어요.

비트 NOT 연산자

비트 NOT 연산자(~)는 숫자의 모든 비트를 반전시킵니다.

lecture image

비트 NOT 연산자는 접두사 연산자로, 공백 없이 연산되는 값 바로 앞에 나타나게 됩니다.

let initialBits: UInt8 = 0b00001111
let invertedBits = ~initialBits  // 11110000과 같아요
swift

UInt8 정수는 8비트를 가지며 0에서 255 사이의 모든 값을 저장할 수 있어요. 이 예제에서는 이진 값 00001111UInt8 정수를 초기화하는데, 첫 4비트는 0으로, 다음 4비트는 1로 설정되죠. 이는 10진수 값 15와 동일합니다.

그런 다음 비트 NOT 연산자를 사용하여 invertedBits라는 새 상수를 만드는데요, 이는 initialBits와 같지만 모든 비트가 반전됩니다. 0은 1이 되고 1은 0이 되는 거죠. invertedBits의 값은 11110000이며, 이는 부호 없는 10진수 값 240과 같답니다.

비트 AND 연산자

비트 AND 연산자(&)는 두 숫자의 비트를 결합합니다. 두 입력 숫자의 비트가 모두 1인 경우에만 1로 설정된 비트를 가진 새 숫자를 반환하죠.

lecture image

아래 예제에서 firstSixBitslastSixBits의 값은 모두 가운데 4비트가 1과 같네요. 비트 AND 연산자는 이들을 결합하여 00111100이라는 숫자를 만드는데, 이는 부호 없는 10진수 값 60과 같아요.

let firstSixBits: UInt8 = 0b11111100
let lastSixBits: UInt8  = 0b00111111
let middleFourBits = firstSixBits & lastSixBits  // 00111100과 같죠
swift

비트 OR 연산자

비트 OR 연산자(|)는 두 숫자의 비트를 비교합니다. 연산자는 입력 숫자 중 하나의 비트가 1이면 비트가 1로 설정된 새 숫자를 반환해요.

lecture image

아래 예제에서 someBitsmoreBits의 값은 서로 다른 비트가 1로 설정되어 있네요. 비트 OR 연산자는 이들을 결합하여 11111110이라는 숫자를 만드는데, 이는 부호 없는 10진수 254와 같답니다.

let someBits: UInt8 = 0b10110010
let moreBits: UInt8 = 0b01011110
let combinedbits = someBits | moreBits  // 11111110과 같아요
swift

비트 XOR 연산자

비트 XOR 연산자 또는 "배타적 OR 연산자"(^)는 두 숫자의 비트를 비교하죠. 연산자는 입력 비트가 다른 경우 1로, 입력 비트가 같은 경우 0으로 설정된 비트를 가진 새 숫자를 반환합니다.

lecture image

아래 예제에서 firstBitsotherBits의 값은 각각 다른 쪽에는 없는 위치에 1로 설정된 비트를 가지고 있어요. 비트 XOR 연산자는 출력 값에서 이 두 비트를 모두 1로 설정하죠. firstBitsotherBits의 다른 모든 비트는 일치하며 출력 값에서 0으로 설정됩니다.

let firstBits: UInt8 = 0b00010100
let otherBits: UInt8 = 0b00000101
let outputBits = firstBits ^ otherBits  // 00010001과 같네요
swift

비트 왼쪽 및 오른쪽 시프트 연산자

비트 왼쪽 시프트 연산자(<<)와 비트 오른쪽 시프트 연산자(>>)는 아래에 정의된 규칙에 따라 숫자의 모든 비트를 특정 자릿수만큼 왼쪽이나 오른쪽으로 이동시킵니다.

비트 왼쪽 및 오른쪽 시프트는 정수에 2를 곱하거나 나누는 효과가 있어요. 정수의 비트를 왼쪽으로 한 자리 이동하면 그 값이 두 배가 되고, 오른쪽으로 한 자리 이동하면 그 값이 반으로 줄어들죠.

부호 없는 정수의 시프트 동작

부호 없는 정수의 비트 시프트 동작은 다음과 같아요.

  1. 기존 비트는 요청된 자릿수만큼 왼쪽이나 오른쪽으로 이동합니다.
  2. 정수의 저장 범위를 벗어나 이동된 비트는 버려집니다.
  3. 원래 비트가 왼쪽이나 오른쪽으로 이동한 후 남은 공간에는 0이 삽입되죠.

이러한 접근 방식을 논리 시프트(logical shift)라고 해요.

아래 그림은 11111111 << 1(11111111을 왼쪽으로 1자리 이동)과 11111111 >> 1(11111111을 오른쪽으로 1자리 이동)의 결과를 보여줍니다. 녹색 숫자는 이동되고, 회색 숫자는 버려지며, 분홍색 0은 삽입되네요.

lecture image

Swift 코드에서 비트 시프트는 다음과 같이 사용됩니다.

let shiftBits: UInt8 = 4   // 이진수로 00000100
shiftBits << 1             // 00001000
shiftBits << 2             // 00010000
shiftBits << 5             // 10000000
shiftBits << 6             // 00000000
shiftBits >> 2             // 00000001
swift

비트 시프트를 사용하여 다른 데이터 유형 내에서 값을 인코딩하고 디코딩할 수도 있죠.

let pink: UInt32 = 0xCC6699
let redComponent = (pink & 0xFF0000) >> 16    // redComponent는 0xCC 또는 204
let greenComponent = (pink & 0x00FF00) >> 8   // greenComponent는 0x66 또는 102
let blueComponent = pink & 0x0000FF           // blueComponent는 0x99 또는 153
swift

이 예제에서는 UInt32 상수인 pink를 사용하여 분홍색의 CSS(Cascading Style Sheets) 색상 값을 저장해요. CSS 색상 값 #CC6699는 Swift의 16진수 표현으로 0xCC6699로 작성되죠. 그런 다음 비트 AND 연산자(&)와 비트 오른쪽 시프트 연산자(>>)를 사용하여 이 색상을 빨간색(CC), 녹색(66) 및 파란색(99) 구성 요소로 분해합니다.

빨간색 구성 요소는 0xCC66990xFF0000 사이의 비트 AND 연산을 수행하여 얻어요. 0xFF0000의 0은 효과적으로 0xCC6699의 두 번째와 세 번째 바이트를 "마스킹"하여 6699가 무시되고 0xCC0000이 결과로 남습니다.

이 숫자는 그 다음 16자리 오른쪽으로 이동하죠(>> 16). 16진수의 각 문자 쌍은 8비트를 사용하므로 16자리 오른쪽으로 이동하면 0xCC00000x0000CC가 됩니다. 이는 0xCC와 같으며 10진수 값은 204예요.

마찬가지로, 녹색 구성 요소는 0xCC66990x00FF00 사이의 비트 AND 연산을 수행하여 얻으며, 출력 값은 0x006600이 됩니다. 이 출력 값은 그 다음 8자리 오른쪽으로 이동하여 0x66이라는 값을 제공하는데, 이는 10진수 값 102를 가지죠.

마지막으로, 파란색 구성 요소는 0xCC66990x0000FF 사이의 비트 AND 연산을 수행하여 얻으며, 출력 값은 0x000099가 됩니다. 0x000099는 이미 0x99와 같고 10진수 값은 153이므로, 이 값은 오른쪽으로 시프트하지 않고 사용되네요.

부호 있는 정수의 시프트 동작

부호 있는 정수의 시프트 동작은 부호 있는 정수가 이진수로 표현되는 방식 때문에 부호 없는 정수보다 더 복잡해요. (아래 예제는 간단히 하기 위해 8비트 부호 있는 정수를 기반으로 하지만, 모든 크기의 부호 있는 정수에 동일한 원칙이 적용됩니다.)

부호 있는 정수는 첫 번째 비트(부호 비트(sign bit)라고 함)를 사용하여 정수가 양수인지 음수인지를 나타내죠. 부호 비트가 0이면 양수를, 1이면 음수를 의미해요.

나머지 비트(값 비트(value bits)라고 함)는 실제 값을 저장합니다. 양수는 부호 없는 정수와 정확히 같은 방식으로 저장되며 0부터 위로 계산하죠. 다음은 숫자 4에 대한 Int8 내부 비트의 모습입니다.

lecture image

부호 비트는 0(즉, "양수")이고 7개의 값 비트는 단순히 이진 표기법으로 작성된 숫자 4예요.

그러나 음수는 다르게 저장됩니다. 값 비트의 수가 n2n제곱에서 절대값을 뺀 값으로 저장되죠. 8비트 숫자는 7개의 값 비트를 가지므로 이는 27제곱 즉 128을 의미해요.

다음은 숫자 -4에 대한 Int8 내부 비트의 모습입니다.

lecture image

이번에는 부호 비트가 1(즉, "음수")이고 7개의 값 비트는 이진 값 124(즉 128 - 4)를 가지네요.

lecture image

이러한 음수 표현 방식을 2의 보수(two's complement) 표현이라고 해요. 음수를 표현하는 특이한 방법처럼 보일 수 있지만 몇 가지 장점이 있답니다.

첫째, -4-1을 더하려면 부호 비트를 포함한 8비트를 모두 표준 이진 덧셈으로 수행하고 완료되면 8비트에 맞지 않는 것은 버리기만 하면 되죠.

lecture image

둘째, 2의 보수 표현은 양수처럼 음수의 비트를 왼쪽과 오른쪽으로 이동할 수 있게 해주며, 여전히 왼쪽으로 이동할 때마다 두 배, 오른쪽으로 이동할 때마다 절반이 됩니다. 이를 달성하기 위해 부호 있는 정수를 오른쪽으로 이동할 때 추가 규칙이 사용되는데요. 부호 있는 정수를 오른쪽으로 이동할 때는 부호 없는 정수와 동일한 규칙을 적용하되 왼쪽의 빈 비트를 0이 아닌 부호 비트로 채웁니다.

lecture image

이 작업은 부호 있는 정수를 오른쪽으로 이동한 후에도 동일한 부호를 유지하도록 보장하며, 산술 시프트(arithmetic shift)로 알려져 있어요.

양수와 음수가 저장되는 특별한 방식 때문에 양수나 음수를 오른쪽으로 이동하면 0에 더 가까워지죠. 이 시프트 동안 부호 비트를 동일하게 유지하면 음수의 값이 0에 가까워질 때 음수로 유지됩니다.

let signedInteger: Int8 = -4
// 이진수로 11111100
 
let shiftRight1 = signedInteger >> 1
// 이진수로 11111110
// -4 >> 1은 -2와 같아요
 
let shiftRight2 = signedInteger >> 2
// 이진수로 11111111
// -4 >> 2는 -1과 같죠
 
let shiftRight3 = signedInteger >> 3
// 이진수로 11111111
// -4 >> 3도 -1과 같네요
swift

위의 예제는 부호 있는 음의 정수를 오른쪽으로 시프트할 때 어떻게 최상위 비트가 1로 채워지고 결과 값이 음수를 유지하면서 0에 가까워지는지 보여줍니다. 반면에 -4 >> 4와 같이 정수의 비트 폭을 초과하여 오른쪽으로 이동하면 모든 비트가 부호 비트(즉, 1)로 채워지고 결과는 항상 -1이 되죠.

부호 있는 정수의 시프트 동작은 약간 까다로울 수 있어요. 하지만 올바르게 사용하면 정수의 부호를 보존하면서 비트를 효율적으로 조작할 수 있는 강력한 도구가 될 수 있답니다.