🔥 비트 연산자
비트 연산자(Bitwise Operators)는 데이터 구조 내의 개별 원시 데이터 비트를 조작할 수 있게 해줍니다. 그래픽 프로그래밍이나 디바이스 드라이버 생성과 같은 저수준 프로그래밍에서 자주 사용되곤 하지요. 또한 사용자 정의 프로토콜을 통해 통신하기 위한 데이터 인코딩 및 디코딩과 같이 외부 소스의 원시 데이터를 다룰 때에도 비트 연산자가 유용할 수 있답니다.
Swift는 아래에 설명된 바와 같이 C에서 발견되는 모든 비트 연산자를 지원하고 있어요.
비트 NOT 연산자
비트 NOT 연산자(~
)는 숫자의 모든 비트를 반전시킵니다.
비트 NOT 연산자는 접두사 연산자로, 공백 없이 연산되는 값 바로 앞에 나타나게 됩니다.
let initialBits: UInt8 = 0b00001111 let invertedBits = ~initialBits // 11110000과 같아요
swift
UInt8
정수는 8비트를 가지며 0
에서 255
사이의 모든 값을 저장할 수 있어요. 이 예제에서는 이진 값 00001111
로 UInt8
정수를 초기화하는데, 첫 4비트는 0
으로, 다음 4비트는 1
로 설정되죠. 이는 10진수 값 15
와 동일합니다.
그런 다음 비트 NOT 연산자를 사용하여 invertedBits
라는 새 상수를 만드는데요, 이는 initialBits
와 같지만 모든 비트가 반전됩니다. 0은 1이 되고 1은 0이 되는 거죠. invertedBits
의 값은 11110000
이며, 이는 부호 없는 10진수 값 240
과 같답니다.
비트 AND 연산자
비트 AND 연산자(&
)는 두 숫자의 비트를 결합합니다. 두 입력 숫자의 비트가 모두 1
인 경우에만 1
로 설정된 비트를 가진 새 숫자를 반환하죠.
아래 예제에서 firstSixBits
와 lastSixBits
의 값은 모두 가운데 4비트가 1
과 같네요. 비트 AND 연산자는 이들을 결합하여 00111100
이라는 숫자를 만드는데, 이는 부호 없는 10진수 값 60
과 같아요.
let firstSixBits: UInt8 = 0b11111100 let lastSixBits: UInt8 = 0b00111111 let middleFourBits = firstSixBits & lastSixBits // 00111100과 같죠
swift
비트 OR 연산자
비트 OR 연산자(|
)는 두 숫자의 비트를 비교합니다. 연산자는 입력 숫자 중 하나의 비트가 1
이면 비트가 1
로 설정된 새 숫자를 반환해요.
아래 예제에서 someBits
와 moreBits
의 값은 서로 다른 비트가 1
로 설정되어 있네요. 비트 OR 연산자는 이들을 결합하여 11111110
이라는 숫자를 만드는데, 이는 부호 없는 10진수 254
와 같답니다.
let someBits: UInt8 = 0b10110010 let moreBits: UInt8 = 0b01011110 let combinedbits = someBits | moreBits // 11111110과 같아요
swift
비트 XOR 연산자
비트 XOR 연산자 또는 "배타적 OR 연산자"(^
)는 두 숫자의 비트를 비교하죠. 연산자는 입력 비트가 다른 경우 1
로, 입력 비트가 같은 경우 0
으로 설정된 비트를 가진 새 숫자를 반환합니다.
아래 예제에서 firstBits
와 otherBits
의 값은 각각 다른 쪽에는 없는 위치에 1
로 설정된 비트를 가지고 있어요. 비트 XOR 연산자는 출력 값에서 이 두 비트를 모두 1
로 설정하죠. firstBits
와 otherBits
의 다른 모든 비트는 일치하며 출력 값에서 0
으로 설정됩니다.
let firstBits: UInt8 = 0b00010100 let otherBits: UInt8 = 0b00000101 let outputBits = firstBits ^ otherBits // 00010001과 같네요
swift
비트 왼쪽 및 오른쪽 시프트 연산자
비트 왼쪽 시프트 연산자(<<
)와 비트 오른쪽 시프트 연산자(>>
)는 아래에 정의된 규칙에 따라 숫자의 모든 비트를 특정 자릿수만큼 왼쪽이나 오른쪽으로 이동시킵니다.
비트 왼쪽 및 오른쪽 시프트는 정수에 2를 곱하거나 나누는 효과가 있어요. 정수의 비트를 왼쪽으로 한 자리 이동하면 그 값이 두 배가 되고, 오른쪽으로 한 자리 이동하면 그 값이 반으로 줄어들죠.
부호 없는 정수의 시프트 동작
부호 없는 정수의 비트 시프트 동작은 다음과 같아요.
- 기존 비트는 요청된 자릿수만큼 왼쪽이나 오른쪽으로 이동합니다.
- 정수의 저장 범위를 벗어나 이동된 비트는 버려집니다.
- 원래 비트가 왼쪽이나 오른쪽으로 이동한 후 남은 공간에는 0이 삽입되죠.
이러한 접근 방식을 논리 시프트(logical shift)라고 해요.
아래 그림은 11111111 << 1
(11111111
을 왼쪽으로 1
자리 이동)과 11111111 >> 1
(11111111
을 오른쪽으로 1
자리 이동)의 결과를 보여줍니다. 녹색 숫자는 이동되고, 회색 숫자는 버려지며, 분홍색 0은 삽입되네요.
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
) 구성 요소로 분해합니다.
빨간색 구성 요소는 0xCC6699
와 0xFF0000
사이의 비트 AND 연산을 수행하여 얻어요. 0xFF0000
의 0은 효과적으로 0xCC6699
의 두 번째와 세 번째 바이트를 "마스킹"하여 6699
가 무시되고 0xCC0000
이 결과로 남습니다.
이 숫자는 그 다음 16자리 오른쪽으로 이동하죠(>> 16
). 16진수의 각 문자 쌍은 8비트를 사용하므로 16자리 오른쪽으로 이동하면 0xCC0000
이 0x0000CC
가 됩니다. 이는 0xCC
와 같으며 10진수 값은 204
예요.
마찬가지로, 녹색 구성 요소는 0xCC6699
와 0x00FF00
사이의 비트 AND 연산을 수행하여 얻으며, 출력 값은 0x006600
이 됩니다. 이 출력 값은 그 다음 8자리 오른쪽으로 이동하여 0x66
이라는 값을 제공하는데, 이는 10진수 값 102
를 가지죠.
마지막으로, 파란색 구성 요소는 0xCC6699
와 0x0000FF
사이의 비트 AND 연산을 수행하여 얻으며, 출력 값은 0x000099
가 됩니다. 0x000099
는 이미 0x99
와 같고 10진수 값은 153
이므로, 이 값은 오른쪽으로 시프트하지 않고 사용되네요.
부호 있는 정수의 시프트 동작
부호 있는 정수의 시프트 동작은 부호 있는 정수가 이진수로 표현되는 방식 때문에 부호 없는 정수보다 더 복잡해요. (아래 예제는 간단히 하기 위해 8비트 부호 있는 정수를 기반으로 하지만, 모든 크기의 부호 있는 정수에 동일한 원칙이 적용됩니다.)
부호 있는 정수는 첫 번째 비트(부호 비트(sign bit)라고 함)를 사용하여 정수가 양수인지 음수인지를 나타내죠. 부호 비트가 0
이면 양수를, 1
이면 음수를 의미해요.
나머지 비트(값 비트(value bits)라고 함)는 실제 값을 저장합니다. 양수는 부호 없는 정수와 정확히 같은 방식으로 저장되며 0
부터 위로 계산하죠. 다음은 숫자 4
에 대한 Int8
내부 비트의 모습입니다.
부호 비트는 0
(즉, "양수")이고 7개의 값 비트는 단순히 이진 표기법으로 작성된 숫자 4
예요.
그러나 음수는 다르게 저장됩니다. 값 비트의 수가 n
인 2
의 n
제곱에서 절대값을 뺀 값으로 저장되죠. 8비트 숫자는 7개의 값 비트를 가지므로 이는 2
의 7
제곱 즉 128
을 의미해요.
다음은 숫자 -4
에 대한 Int8
내부 비트의 모습입니다.
이번에는 부호 비트가 1
(즉, "음수")이고 7개의 값 비트는 이진 값 124
(즉 128 - 4
)를 가지네요.
이러한 음수 표현 방식을 2의 보수(two's complement) 표현이라고 해요. 음수를 표현하는 특이한 방법처럼 보일 수 있지만 몇 가지 장점이 있답니다.
첫째, -4
에 -1
을 더하려면 부호 비트를 포함한 8비트를 모두 표준 이진 덧셈으로 수행하고 완료되면 8비트에 맞지 않는 것은 버리기만 하면 되죠.
둘째, 2의 보수 표현은 양수처럼 음수의 비트를 왼쪽과 오른쪽으로 이동할 수 있게 해주며, 여전히 왼쪽으로 이동할 때마다 두 배, 오른쪽으로 이동할 때마다 절반이 됩니다. 이를 달성하기 위해 부호 있는 정수를 오른쪽으로 이동할 때 추가 규칙이 사용되는데요. 부호 있는 정수를 오른쪽으로 이동할 때는 부호 없는 정수와 동일한 규칙을 적용하되 왼쪽의 빈 비트를 0이 아닌 부호 비트로 채웁니다.
이 작업은 부호 있는 정수를 오른쪽으로 이동한 후에도 동일한 부호를 유지하도록 보장하며, 산술 시프트(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
이 되죠.
부호 있는 정수의 시프트 동작은 약간 까다로울 수 있어요. 하지만 올바르게 사용하면 정수의 부호를 보존하면서 비트를 효율적으로 조작할 수 있는 강력한 도구가 될 수 있답니다.