Bitwise Operators

비트와이즈 연산자는 저수준 최적화, 데이터 압축, 하드웨어 제어 등 다양한 영역에서 강력한 도구이다.
모든 비트를 직접 다루기 때문에 신중하게 사용해야 하지만, 잘 활용하면 성능과 메모리 효율성에서 큰 이점을 얻을 수 있다.

현대 프로그래밍에서는 추상화 수준이 높아졌지만, 시스템 프로그래밍, 임베디드 시스템, 게임 개발, 데이터베이스 엔진 등에서 비트와이즈 연산은 여전히 핵심적인 역할을 수행한다. 효율적인 프로그래밍을 위해 비트와이즈 연산자의 활용법을 숙지하는 것이 좋다.

비트와이즈 연산자의 기본 개념

비트와이즈 연산자는 정수 값을 이진수(비트)로 표현하여 비트 단위로 연산을 수행한다.
컴퓨터의 모든 데이터는 결국 이진수로 저장되기 때문에, 비트 단위 조작은 저수준(low-level) 프로그래밍에서 특히 중요하다.

비트와이즈 연산자의 종류

  1. AND (&) - 두 비트가 모두 1일 때만 결과가 1
  2. OR (|) - 두 비트 중 하나라도 1이면 결과가 1
  3. XOR (^) - 두 비트가 서로 다를 때만 결과가 1
  4. NOT (~) - 비트를 반전 (1→0, 0→1)
  5. Left Shift («) - 비트를 왼쪽으로 이동
  6. Right Shift (») - 비트를 오른쪽으로 이동
  7. Unsigned Right Shift (»>) - 부호 비트와 관계없이 0을 채우며 오른쪽으로 이동 (JavaScript 등 일부 언어에서 지원)

각 연산자의 상세 설명

AND 연산자 (&)

두 비트가 모두 1일 때만 결과가 1이 된다.

1
2
3
4
   1010 (10)
 & 1100 (12)
   ----
   1000 (8)

AND 연산자는 주로 다음과 같은 용도로 사용된다:

1
2
3
4
5
6
7
# 숫자가 짝수인지 확인 (최하위 비트가 0이면 짝수)
def is_even(num):
    return (num & 1) == 0  # 최하위 비트만 추출하여 검사

# 실제 사용 예
print(is_even(42))  # True
print(is_even(43))  # False

OR 연산자 (|)

두 비트 중 하나라도 1이면 결과가 1이 된다.

1
2
3
4
   1010 (10)
 | 1100 (12)
   ----
   1110 (14)

OR 연산자는 주로 다음과 같은 용도로 사용됩니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 권한 설정 예제
const READ = 4;     // 100 (이진수)
const WRITE = 2;    // 010
const EXECUTE = 1;  // 001

// 읽기와 실행 권한 부여
let permissions = READ | EXECUTE;  // 5 (101)

// 권한 확인
if (permissions & WRITE) {
    console.log("쓰기 권한이 있습니다");
} else {
    console.log("쓰기 권한이 없습니다");  // 이 메시지 출력
}

XOR 연산자 (^)

두 비트가 서로 다를 때만 결과가 1이 된다.

1
2
3
4
   1010 (10)
 ^ 1100 (12)
   ----
   0110 (6)

XOR 연산자는 주로 다음과 같은 용도로 사용된다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# XOR 암호화 예제
def xor_encrypt(data, key):
    """XOR 연산을 사용한 간단한 암호화"""
    result = bytearray(len(data))
    for i in range(len(data)):
        result[i] = data[i] ^ key[i % len(key)]
    return result

# 값 교환 (임시 변수 없이)
a = 5
b = 9
a = a ^ b  # a = 5 ^ 9
b = a ^ b  # b = (5 ^ 9) ^ 9 = 5
a = a ^ b  # a = (5 ^ 9) ^ 5 = 9

NOT 연산자 (~)

각 비트를 반전시킨다 (1→0, 0→1).

1
2
3
~1010 (10)
-----
 0101 (-11) - 2의 보수 표현으로 인해 부호가 있는 정수에서는 -(x+1) 결과가 됨

NOT 연산자는 주로 다음과 같은 용도로 사용된다:

1
2
3
4
5
6
7
// 비트 반전과 마스킹을 이용한 예제
const clearBit = (num, position) => {
    // ~(1 << position)은 해당 위치만 0이고 나머지는 모두 1인 마스크 생성
    return num & ~(1 << position);
};

console.log(clearBit(10, 1).toString(2));  // 1000 (이진수로 8)

2.5. Left Shift 연산자 («)

비트를 왼쪽으로 이동시킨다. 오른쪽에는 0이 채워진다.

1
2
3
1010 (10) << 2
---------
101000 (40)

Left Shift는 주로 다음과 같은 용도로 사용된다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 2의 거듭제곱 계산
def power_of_two(n):
    """2의 n승 계산 (1 << n)"""
    return 1 << n

# RGB 색상 생성 (비트 시프트 활용)
def create_rgb(r, g, b):
    """RGB 값을 32비트 정수로 변환"""
    return (r << 16) | (g << 8) | b

# 사용 예
red = create_rgb(255, 0, 0)  # FF0000
print(hex(red))  # 0xff0000

Right Shift 연산자 (», »>)

비트를 오른쪽으로 이동시킨다.
두 종류가 있다:

  1. 산술 시프트 (»): 부호 비트(MSB)를 보존하여 이동 (부호 있는 정수)
  2. 논리 시프트 (»>): 부호 무시하고 항상 0을 채움 (JavaScript 등 일부 언어에서 지원)
1
2
3
4
5
6
7
8
9
# 산술 시프트
1010 (10) >> 2
-------
0010 (2)

# 논리 시프트 (JavaScript)
11111010 (-6) >>> 2
---------
00111110 (62)

Right Shift는 주로 다음과 같은 용도로 사용된다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// RGBA에서 개별 색상 성분 추출
function extractComponents(rgba) {
    const r = (rgba >> 24) & 0xFF;
    const g = (rgba >> 16) & 0xFF;
    const b = (rgba >> 8) & 0xFF;
    const a = rgba & 0xFF;
    return { r, g, b, a };
}

// 사용 예
const color = 0xFF5A1CB8;  // RGBA 색상
const { r, g, b, a } = extractComponents(color);
console.log(`R: ${r}, G: ${g}, B: ${b}, A: ${a}`);  // R: 255, G: 90, B: 28, A: 184

비트 연산자 정리

연산자기호설명예시결과주요 용도
비트 AND&두 비트가 모두 1일 때만 15 & 3
(0101 & 0011)
1
(0001)
• 비트 마스킹
• 특정 비트 추출
• 짝수/홀수 확인
비트 OR|두 비트 중 하나라도 1이면 15 | 3
(0101 | 0011)
7
(0111)
• 비트 설정
• 여러 플래그 결합
• 권한 부여
비트 XOR^두 비트가 서로 다를 때만 15 ^ 3
(0101 ^ 0011)
6
(0110)
• 비트 토글
• 간단한 암호화
• 값 교환
비트 NOT~모든 비트 반전 (0→1, 1→0)~5
(~0101)
-6
(1..)
• 비트 반전
• 마스크 반전
• 2의 보수
왼쪽 시프트«비트를 왼쪽으로 이동
(오른쪽은 0으로 채움)
5 « 2
(0101 « 2)
20
(10100)
• 빠른 곱셈 (2^n)
• 비트 마스크 생성
• 값 압축
오른쪽 시프트»비트를 오른쪽으로 이동
(왼쪽은 부호 비트로 채움)
5 » 2
(0101 » 2)
1
(0001)
• 빠른 나눗셈 (2^n)
• 비트 추출
• 부호 유지 연산
논리 오른쪽 시프트»>부호 무시하고 오른쪽으로 이동
(왼쪽은 0으로 채움)
-5 »> 2
(1.. »> 2)
1073741822
(0…..)
• 부호 무시 시프트
• 무부호 정수 처리
• 해시 함수 구현

연산자 우선순위

비트 연산자의 우선순위는 다음과 같다
(위에서 아래로 우선순위 높음→낮음):

  1. 비트 NOT (~)
  2. 시프트 연산자 («, », »>)
  3. 비트 AND (&)
  4. 비트 XOR (^)
  5. 비트 OR (|)

성능과 메모리 측면의 이점

비트와이즈 연산자는 다음과 같은 이점을 제공한다:

  1. 연산 속도: 하드웨어 수준에서 직접 지원되어 매우 빠름
  2. 메모리 효율성: 여러 불리언 값을 단일 정수에 저장 가능
  3. 대역폭 최적화: 네트워크 전송 시 데이터 압축 가능
  4. 캐시 효율성: 적은 메모리 사용으로 캐시 히트율 향상

최적화와 비트 트릭

2의 거듭제곱 확인

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 어떤 숫자가 2의 거듭제곱인지 확인
def is_power_of_two(n):
    """
    n이 2의 거듭제곱인지 확인
    n & (n-1)이 0이면 2의 거듭제곱임
    """
    return n > 0 and (n & (n - 1)) == 0

# 사용 예
print(is_power_of_two(16))  # True (10000)
print(is_power_of_two(18))  # False (10010)

홀수/짝수 확인

1
2
3
4
5
6
7
8
// 홀수/짝수 확인 (최하위 비트 검사)
function isEven(n) {
    return (n & 1) === 0;
}

function isOdd(n) {
    return (n & 1) === 1;
}

부호 확인

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 부호 확인 (최상위 비트 검사)
def sign(x):
    """
    x의 부호 반환:
    x > 0 이면 1
    x < 0 이면 -1
    x = 0 이면 0
    """
    return (x > 0) - (x < 0)
    
    # 비트 연산 접근법 (32비트 정수 가정)
    # return 1 | (x >> 31)  # x가 음수면 -1, 아니면 1

모듈로 연산 최적화

1
2
3
4
5
6
7
8
9
// 2의 거듭제곱으로 나눈 나머지 계산 최적화
function modPowerOfTwo(n, mod) {
    // mod는 2의 거듭제곱이어야 함 (예: 2, 4, 8, 16, …)
    return n & (mod - 1);
}

// 사용 예
console.log(modPowerOfTwo(10, 8));  // 2 (10 % 8 = 2)
console.log(modPowerOfTwo(20, 16));  // 4 (20 % 16 = 4)

고급 활용 기법

6비트 조작 알고리즘

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 비트 반전 (endian-neutral)
def reverse_bits(n, bit_count=32):
    """
    n의 비트를 뒤집음 (bit_count 비트 기준)
    """
    result = 0
    for i in range(bit_count):
        result = (result << 1) | (n & 1)
        n >>= 1
    return result

# 사용 예
print(bin(reverse_bits(0b00000000000000000000000000000101)))
# 0b10100000000000000000000000000000

SIMD 스타일 병렬 처리

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 4개의 8비트 값을 병렬로 처리
function incrementBytes(n) {
    // 모든 8비트 경계를 넘는 자리올림 방지를 위한 마스크
    const mask = 0x01010101;
    
    // 각 바이트에 1을 더함
    return n + mask & ~(mask << 8);
}

// 사용 예
console.log(incrementBytes(0x01020304).toString(16));  // "01020405"

비트 필드 자료구조 활용

일반적인 집합은 각 요소를 개별적으로 저장한다. 반면, BitSet은 각 요소의 존재 여부를 단일 비트로 표현한다.
이는 특히 정수 집합을 다룰 때 매우 효율적인 방법이다.

예를 들어, 0부터 31까지의 정수를 저장하려면 일반적인 방법은 최대 32개의 정수 객체가 필요하지만, BitSet은 단 32비트(4바이트)만으로 충분하다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 비트 필드를 사용한 효율적인 집합 구현
class BitSet:
    def __init__(self, max_val):
        self.data = [0] * ((max_val >> 5) + 1)  # 32비트 단위로 저장
    
    def add(self, val):
        """집합에 값 추가"""
        self.data[val >> 5] |= (1 << (val & 31))
    
    def remove(self, val):
        """집합에서 값 제거"""
        self.data[val >> 5] &= ~(1 << (val & 31))
    
    def contains(self, val):
        """값이 집합에 포함되어 있는지 확인"""
        return bool(self.data[val >> 5] & (1 << (val & 31)))
    
    def __str__(self):
        """집합의 내용을 문자열로 표현"""
        return str([i for i in range(len(self.data) * 32) if self.contains(i)])

# 사용 예
bs = BitSet(100)
bs.add(5)
bs.add(10)
bs.add(42)
# `bs.data[0]` = `0..` (5번째와 10번째 비트가 1)
# `bs.data[1]` = `0..` (42번째 비트가 1, 실제로는 (42-32)=10번째 비트)
# `bs.data[2]` = 0
# `bs.data[3]` = 0
print(bs.contains(10))  # True
print(bs.contains(11))  # False
bs.remove(10)
print(bs.contains(10))  # False

실제 활용 사례

비트 플래그와 상태 관리

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 비트 플래그를 이용한 상태 관리
# 8비트로 8가지 상태 관리 (공간 효율성)
STATE_ACTIVE = 1 << 0    # 00000001
STATE_VISIBLE = 1 << 1   # 00000010
STATE_ENABLED = 1 << 2   # 00000100
STATE_SELECTED = 1 << 3  # 00001000
STATE_FOCUSED = 1 << 4   # 00010000
STATE_MOVING = 1 << 5    # 00100000
STATE_RESIZING = 1 << 6  # 01000000
STATE_FULLSCREEN = 1 << 7  # 10000000

# 여러 상태 설정
current_state = STATE_ACTIVE | STATE_VISIBLE | STATE_ENABLED  # 00000111

# 상태 확인
is_active = bool(current_state & STATE_ACTIVE)    # True
is_selected = bool(current_state & STATE_SELECTED)  # False

# 상태 토글
current_state ^= STATE_VISIBLE  # VISIBLE 상태 토글 (꺼짐)

# 상태 제거
current_state &= ~STATE_ENABLED  # ENABLED 상태 제거

비트 필드와 데이터 압축

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// IPv4 주소를 단일 32비트 정수로 압축
function packIPv4(a, b, c, d) {
    return (a << 24) | (b << 16) | (c << 8) | d;
}

// 32비트 정수에서 IPv4 주소 추출
function unpackIPv4(ip) {
    return [
        (ip >> 24) & 0xFF,
        (ip >> 16) & 0xFF,
        (ip >> 8) & 0xFF,
        ip & 0xFF
    ];
}

// 사용 예
const packedIP = packIPv4(192, 168, 1, 1);  // 3232235777
console.log(unpackIPv4(packedIP));  // [192, 168, 1, 1]

해시 함수 구현

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 간단한 비트 해시 함수 (예: 문자열 해싱)
def simple_hash(s, table_size):
    """간단한 비트 연산 기반 해시 함수"""
    h = 0
    for c in s:
        h = (h << 5) ^ (h >> 27) ^ ord(c)  # 비트 회전과 XOR 조합
    return h % table_size

# 사용 예
print(simple_hash("hello", 1000))  # 어떤 해시 값 출력
print(simple_hash("hello!", 1000))  # 다른 해시 값 출력

비트 수 세기 (비트 인구 계산)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 2진수에서 1의 개수 세기 (Brian Kernighan의 알고리즘)
function countBits(n) {
    let count = 0;
    while (n) {
        n &= (n - 1);  // 가장 오른쪽 1 비트 제거
        count++;
    }
    return count;
}

// 사용 예
console.log(countBits(7));   // 3 (111)
console.log(countBits(15));  // 4 (1111)

용어 정리

용어설명

참고 및 출처