Skip to content
블로그로 돌아가기
튜토리얼

비트 연산 완벽 가이드: AND·OR·XOR·시프트·마스크 온라인 정리

AND, OR, XOR, 시프트, 2의 보수, 비트 마스크, 플래그를 JS·파이썬·Go·C 예제로 익히는 온라인 비트 연산 실전 가이드.

17분 소요

비트 연산 실전: AND, OR, XOR, 시프트, 마스크

레거시 PostgreSQL 마이그레이션 파일을 열었더니 permissions & 0b100 이라는 코드가 보입니다. 동료는 32개의 불리언을 하나의 정수에 패킹한 피처 플래그 시스템을 배포합니다. Kubernetes 서브넷 계산 결과로 192.168.1.0/24가 나오는데, 코드에서 네트워크 주소를 추출해야 합니다. 서로 다른 세 가지 상황이지만 그 밑바탕에는 하나의 기술이 있습니다. 바로 비트 연산입니다.

애플리케이션 레이어 개발자는 웹 앱에서 &^를 쓸 일이 거의 없다가, 어느 순간 갑자기 필요해집니다. 여섯 가지 비트 연산자, 2의 보수, 외워 둘 만한 아홉 가지 패턴, 그리고 (특히 자바스크립트에서) 발목을 잡는 언어별 함정을 다룹니다. 코드는 자바스크립트, 파이썬, Go, C로 제공되고 예제는 모두 실행 가능합니다.

진법 변환기를 다른 탭에 열어 두세요. 몇몇 절에서는 숫자를 입력해 비트 패턴이 바뀌는 모습을 직접 볼 수 있습니다.

2026년에도 비트 연산이 여전히 중요한 이유

고수준 언어가 비트 연산을 없앤 것이 아니라, 연산이 일어나는 위치를 가렸을 뿐입니다. 인지하든 하지 않든 오늘도 쓰고 있는 곳 몇 군데입니다.

  • PostgreSQL의 Row-Level Security는 ACL 권한(SELECT, INSERT, UPDATE, DELETE, …)을 정수 하나에 패킹한 비트맵으로 관리합니다.
  • 리눅스 capability는 과거의 “루트 아니면 아무것도 아님” 모델을 |로 조합하는 40여 개의 권한 비트로 대체했습니다.
  • JWT 알고리즘 헤더는 해시 알고리즘을 작은 필드에 인코딩하며, 라이브러리 레이어에서는 비트 수준 비교가 흔하게 사용됩니다.
  • Snowflake, ULID, UUIDv7은 좌측 시프트를 사용해 타임스탬프, 머신 ID, 시퀀스 번호를 하나의 64비트 혹은 128비트 정수로 패킹합니다.
  • Redis의 BITCOUNTBITOP은 카디널리티 추정이나 A/B 버킷팅을 위해 애플리케이션 코드가 직접 쓸 수 있는 비트 연산 기본 연산자를 노출합니다.
  • 이미지 처리 코드는 32비트 RGBA 픽셀을 읽고 &>>로 각 채널을 추출합니다.

비트 연산은 CPU 명령어 수준에서 여전히 O(1)입니다. 불리언 32개를 정수 하나에 패킹하면 31바이트가 줄고, 더 중요하게는 “32개 플래그 중 하나라도 켜져 있는가”를 != 0 한 번으로 확인할 수 있습니다.

먼저 알아야 할 2진수 기초

이 글은 2진수 동작 원리를 안다고 가정합니다. 복습이 필요하면 진법 변환 가이드를 먼저 보세요.

용어부터 간단히 정리합니다.

  • 비트(bit)는 0 또는 1입니다.
  • 니블(nibble)은 4비트입니다(16진수 한 자리).
  • 바이트(byte)는 8비트입니다.
  • 워드(word)는 보통 CPU에 따라 32비트 또는 64비트입니다.

대부분의 언어에서 정수는 8, 16, 32, 64비트처럼 고정 너비를 가집니다. 시프트 연산은 비트를 너비 바깥으로 밀어낼 수 있고, 부호 있는 정수에서는 부호 비트가 가장 왼쪽에 자리합니다. 그래서 너비는 비트 연산 결과에 큰 영향을 줍니다.

한번 해 봅시다. 진법 변환기를 열고 10진수 170을 입력하면 2진수로 10101010이 나옵니다. 이 교차 패턴은 아래에서 여러 번 다시 등장합니다.

여섯 가지 비트 연산자

주류 언어는 모두 같은 여섯 가지 연산자를 제공하며, 문법상 미세한 차이만 있습니다. &, |, ^, ~, <<, >> 기호는 자바스크립트, 파이썬, Go, Rust, C, C++, Java, C#에서 그대로 동작합니다. 자바스크립트에는 한 가지가 추가됩니다. 부호 없는 우측 시프트인 >>>입니다.

AND (&): 비트 필터

두 입력 비트가 모두 1일 때만 출력 비트가 1이 됩니다.

ABA & B
000
010
100
111

AND는 게이트처럼 생각할 수 있습니다. 양쪽 피연산자에서 모두 켜져 있는 비트만 살아남습니다. 가장 흔한 용도는 마스킹으로, 일부 비트는 유지하고 나머지는 0으로 만드는 것입니다.

// Extract the low 4 bits (the rightmost nibble)
const value = 0b11010110;   // 214
const low4  = value & 0x0F; // 0b00000110 = 6

// Check if a number is odd
const isOdd = (n) => (n & 1) === 1;
isOdd(7);  // true
isOdd(42); // false
# Same in Python
value = 0b11010110
low4 = value & 0x0F  # 6

def is_odd(n):
    return (n & 1) == 1

OR (|): 비트 설정자

둘 중 하나라도 입력 비트가 1이면 출력 비트는 1이 됩니다.

ABA | B
000
011
101
111

OR은 플래그를 결합합니다. READ = 1, WRITE = 2, EXECUTE = 4로 정의해 두면 READ | WRITE3이 되며 두 권한이 모두 활성화된 상태입니다.

const READ  = 0b001;
const WRITE = 0b010;
const EXEC  = 0b100;

const rw = READ | WRITE;  // 0b011 = 3
READ, WRITE, EXEC = 0b001, 0b010, 0b100
rw = READ | WRITE  # 3

XOR (^): 비트 토글

두 입력 비트가 서로 다를 때 출력 비트가 1이 됩니다.

ABA ^ B
000
011
101
110

XOR에는 컴퓨터 과학에서 가장 영리한 트릭 몇 가지를 떠받치는 세 가지 대수적 성질이 있습니다.

  • a ^ a = 0: 자기 자신과 XOR하면 상쇄됩니다.
  • a ^ 0 = a: 0과 XOR하면 값이 그대로 유지됩니다(항등).
  • a ^ b ^ a = b: XOR은 자기 자신이 역연산입니다.

마지막 성질은 XOR이 패리티 검사, 스트림 암호, 그리고 악명 높은 “배열에서 유일하게 중복되지 않는 숫자 찾기” 인터뷰 문제에 등장하는 이유이기도 합니다.

// Find the one unique number in an array where every other number appears twice
const findUnique = (arr) => arr.reduce((a, b) => a ^ b, 0);
findUnique([4, 1, 2, 1, 2]);  // 4
from functools import reduce
from operator import xor
find_unique = lambda arr: reduce(xor, arr, 0)
find_unique([4, 1, 2, 1, 2])  # 4

NOT (~): 비트 반전자

단항 ~는 모든 비트를 뒤집습니다. 0은 1이, 1은 0이 됩니다.

~0b00001111  // -16  (JavaScript coerces to 32-bit signed)
~5           // -6
~5  # -6
// Go uses ^ as unary bitwise NOT, watch out
var x int8 = 5
fmt.Println(^x)  // -6

~5의 결과는 주류 언어 모두에서 -6이고, 초보자에게는 당황스러운 결과입니다. 이유는 다음 절의 2의 보수입니다. 지금은 2의 보수를 쓰는 모든 언어에서 ~x-(x + 1)과 같다는 점만 기억하면 됩니다.

좌측 시프트 (<<): 2의 거듭제곱 곱셈기

x << nx의 모든 비트를 n 자리만큼 왼쪽으로 옮기고, 오른쪽은 0으로 채웁니다. 수학적으로는 2ⁿ을 곱하는 것과 같습니다.

1 << 0   // 1   (2^0)
1 << 1   // 2   (2^1)
1 << 3   // 8   (2^3)
1 << 10  // 1024 (2^10 = 1 KiB)

// Building bit flags
const FLAG_ADMIN    = 1 << 0;
const FLAG_EDITOR   = 1 << 1;
const FLAG_REVIEWER = 1 << 2;

1 << n의 편리한 점은 n 위치에 비트 하나만 켜져 있는 값을 만들어 준다는 것입니다. 바로 그 비트가 플래그가 됩니다.

오버플로에 주의하세요. 자바스크립트의 비트 연산자는 32비트 부호 있는 정수에서 동작하기 때문에, 1 << 312147483648이 아니라 -2147483648이 됩니다.

우측 시프트 (>> vs >>>): 나눗셈인가 0으로 채움인가

우측 시프트는 비트를 오른쪽으로 옮깁니다. 문제는 비어 버린 가장 왼쪽 자리를 무엇으로 채우는가입니다.

  • >> (산술 우측 시프트)는 부호 비트를 유지합니다. 음수는 그대로 음수로 남습니다.
  • >>> (논리 혹은 부호 없는 우측 시프트)는 0으로 채웁니다. 전용 연산자로 이걸 갖춘 언어는 자바스크립트뿐입니다.
-8 >> 1   // -4   (sign bit preserved)
-8 >>> 1  // 2147483644  (sign bit treated as a data bit)

8 >> 1    // 4
8 >> 2    // 2

C에서는 부호 있는 타입의 >>가 산술인지 논리인지 구현 정의입니다. 대부분의 컴파일러가 산술 시프트를 하지만, 확인 없이 믿으면 안 됩니다. Go는 시프트량이 반드시 부호 없는 정수여야 하며, 부호 있는 타입과 부호 없는 타입을 엄격히 구분합니다. 파이썬은 고정 너비 정수가 없어 >>>도 없습니다.

2의 보수: 컴퓨터가 음수를 표현하는 방식

비트가 0과 1뿐이면 -5를 어떻게 인코딩할까요? 1960년대에 합의된 답이 2의 보수이고, 모든 현대 CPU가 이를 씁니다.

한 비트를 부호 표시에 할당하는 소박한 접근에는 두 가지 문제가 있습니다. +0-0이 모두 생겨 어색하고, 덧셈·뺄셈 회로가 부호 비트를 검사해야 하므로 하드웨어가 복잡해집니다. 2의 보수는 둘 다 해결합니다.

규칙은 간단합니다.

  1. 양수의 2진수 표현을 적습니다.
  2. 모든 비트를 뒤집습니다(이것이 “1의 보수”입니다).
  3. 1을 더합니다.

-5를 8비트 2의 보수로 인코딩하는 예시는 다음과 같습니다.

 5 in binary:        0000 0101
 flip all bits:      1111 1010   (this is -6 in two's complement!)
 add 1:              1111 1011   ← this is -5

진법 변환기에서 10진수 251을 입력하면 2진수 11111011이 나옵니다. 이를 8비트 부호 있는 정수로 보면 -5, 부호 없는 정수로 보면 251입니다. 비트는 같고 해석만 다릅니다.

이로써 앞서 나온 ~5 = -6의 수수께끼가 풀립니다. 비트 NOT은 비트를 뒤집어 1의 보수를 만들고, 2의 보수는 1의 보수에 1을 더한 값입니다. 그러므로 다음이 성립합니다.

~x   = -(x + 1)        // identity in any two's complement language
~5   = -6
~(-3) = 2

n비트 부호 있는 정수의 표현 가능 범위는 -2ⁿ⁻¹부터 2ⁿ⁻¹ − 1까지입니다. 8비트 부호 있는 정수는 -128부터 127까지, 32비트 부호 있는 정수는 대략 -21억부터 +21억까지를 담을 수 있습니다.

반드시 알아야 할 비트 조작 패턴

아래 아홉 가지가 앞으로 작성할 비트 조작의 95% 정도를 덮습니다. 시스템 코드 곳곳에서 다시 만나게 되니 외워 두면 좋습니다.

비트 설정: x | (1 << n)

다른 비트는 그대로 두고 n번 비트만 켭니다.

let flags = 0b0100;
flags = flags | (1 << 0);  // 0b0101

비트 해제: x & ~(1 << n)

다른 비트는 그대로 두고 n번 비트만 끕니다. ~(1 << n)n번 비트 하나만 빼고 나머지가 모두 켜진 마스크입니다.

let flags = 0b0111;
flags = flags & ~(1 << 1);  // 0b0101

비트 토글: x ^ (1 << n)

현재 상태와 무관하게 n번 비트를 뒤집습니다.

let flags = 0b0100;
flags = flags ^ (1 << 2);  // 0b0000
flags = flags ^ (1 << 2);  // 0b0100 again

비트 확인: (x >> n) & 1

n번 비트가 켜져 있으면 1, 아니면 0을 반환합니다. 동치 형태로는 (x & (1 << n)) !== 0이 있습니다.

const flags = 0b0101;
const isBit2Set = (flags >> 2) & 1;  // 1

가장 낮은 켜진 비트 분리: x & -x

x의 가장 오른쪽 1 비트만 남긴 값을 만들어 줍니다. 2의 보수에서 -x~x + 1과 같기 때문에, 가장 낮은 켜진 비트까지의 모든 비트가 뒤집힌다는 점에서 이 트릭이 성립합니다.

const x = 0b10110100;
const lowest = x & -x;  // 0b00000100 = 4

이 트릭은 O(log n) 접두 합(prefix sum) 계산에 쓰이는 펜윅 트리(Fenwick tree, Binary Indexed Tree)의 핵심 원리입니다.

켜진 비트 개수 세기 (popcount)

정수에 들어 있는 1 비트 개수를 세는 연산입니다. 요즘은 대부분의 언어에 네이티브 함수가 있습니다.

// JavaScript (BigInt or manual)
const popcount = (n) => {
  let count = 0;
  while (n) { count += n & 1; n >>>= 1; }
  return count;
};
popcount(0b10110100);  // 4
# Python 3.10+
(0b10110100).bit_count()  # 4
// Go
import "math/bits"

bits.OnesCount(0b10110100) // 4

임시 변수 없는 XOR 스왑

세 번째 변수 없이 두 정수를 바꾸는 고전적인 트릭입니다. 프로덕션에서는 쓰지 마세요. 임시 변수보다 느리고, ab가 같은 메모리를 가리키면 깨집니다. 원리만 알아 두세요.

let 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
// a = 9, b = 5

2의 거듭제곱 판별: (x & (x - 1)) === 0

2의 거듭제곱은 정확히 하나의 비트만 켜져 있습니다. 1을 빼면 그 비트가 꺼지고 아래쪽 모든 비트가 켜집니다. AND를 취하면 2의 거듭제곱(과 0 자체)에 대해서만 0이 되므로, x > 0 조건을 덧붙여 0을 걸러냅니다.

const isPow2 = (x) => x > 0 && (x & (x - 1)) === 0;
isPow2(16);  // true
isPow2(17);  // false

빠른 홀짝 판별: x & 1

일부 언어에서는 x % 2보다 빠르며, 컴파일러 최적화를 거친 뒤에는 동일해지는 언어도 있습니다. 가독성이 크게 중요하지 않은 핫 루프에서는 의미가 있습니다.

const isOdd = (x) => (x & 1) === 1;

실제 코드에서 쓰이는 비트 마스크 플래그

앞에서 본 패턴은 프로덕션 코드에서 매일같이 등장합니다. 자주 마주치는 네 가지 사례를 살펴봅니다.

32개 불리언에 담은 피처 플래그

32개 필드의 불리언 구조체 대신 정수 하나에 패킹합니다.

const FLAGS = {
  DARK_MODE:      1 << 0,
  NEW_NAV:        1 << 1,
  AI_SUGGESTIONS: 1 << 2,
  BETA_EDITOR:    1 << 3,
  // ... up to 1 << 31
};

let userFlags = 0;
userFlags |= FLAGS.DARK_MODE | FLAGS.AI_SUGGESTIONS;  // opt in

if (userFlags & FLAGS.AI_SUGGESTIONS) {
  showSuggestions();
}

userFlags &= ~FLAGS.DARK_MODE;  // opt out

이 패턴은 32개의 불리언을 4바이트에 저장하고, AND 한 번으로 임의의 부분집합을 조회할 수 있게 해 줍니다. 32개 컬럼 대신 하나만 쓰면 되므로 데이터베이스 측면에서도 선호됩니다.

Unix 파일 권한

chmod 755는 비트 연산입니다. 세 자리 8진수는 각각 세 비트씩 묶인 트리플 세 개에 대응합니다.

7 = 111  (owner:   rwx)
5 = 101  (group:   r-x)
5 = 101  (others:  r-x)

확인해 보세요. 진법 변환기에서 입력 진법을 8진수로 두고 755를 입력하면 2진수 출력이 111101101입니다. 파일 시스템이 권한 필드를 저장하는 방식도 이와 같습니다.

“그룹 쓰기”만 추가 설정하려면 다음과 같이 합니다.

const perms = 0o755;
const withGroupWrite = perms | 0o020;  // 0o775

IP 서브넷 마스킹

192.168.1.10/24가 주어졌을 때, 마스크와 AND하여 네트워크 주소를 추출합니다.

const ip      = 0xC0A8010A;  // 192.168.1.10
const mask    = 0xFFFFFF00;  // 255.255.255.0 (/24)
const network = ip & mask;   // 0xC0A80100 = 192.168.1.0

패킹된 ID: Snowflake

트위터의 Snowflake는 64비트 정수 하나에 타임스탬프, 머신 ID, 시퀀스를 패킹합니다.

┌─ 1 bit ─┬─── 41 bits ───┬─ 10 bits ─┬─ 12 bits ─┐
│  sign   │   timestamp   │ machine   │   seq     │
└─────────┴───────────────┴───────────┴───────────┘

ID 인코딩은 시프트 두 번과 OR 두 번이면 됩니다.

const id = (BigInt(timestamp) << 22n) |
           (BigInt(machineId) << 12n) |
            BigInt(sequence);

디코딩은 반대 방향, 즉 우측 시프트와 마스크를 씁니다. Snowflake, ULID, UUIDv7 중 무엇을 언제 고를지는 분산 ID 비교: UUID v4/v7/ULID/Snowflake에서 다룹니다.

언어별 함정

자바스크립트: 32비트 강제 변환 함정

자바스크립트는 비트 연산이 일어나기 전에 피연산자를 32비트 부호 있는 정수로 변환하고, 연산이 끝나면 다시 Number로 되돌립니다. 2³¹ − 1 = 2147483647을 초과하는 값은 오버플로가 발생합니다.

2147483647 | 0   // 2147483647   (still fine)
2147483648 | 0   // -2147483648  (overflowed!)
4294967295 | 0   // -1           (all bits set, interpreted signed)

64비트 작업이 필요하다면 BigInt를 사용하세요. 너비 제한이 없는 별도의 비트 연산자 집합이 있습니다.

(2n ** 40n) | 1n  // 1099511627777n

연산자 우선순위 버그

실제 세계에서 가장 흔한 비트 연산 버그 중 하나입니다.

// Buggy: reads as (x & (1 == 0)) because == binds tighter than &
if (x & 1 == 0) { /* ... */ }

// Correct: parenthesize
if ((x & 1) == 0) { /* ... */ }

C, 자바스크립트, 파이썬, Go, 그리고 대부분의 파생 언어에서는 비교 연산자가 비트 AND/OR/XOR보다 우선순위가 높습니다. 의심스러우면 괄호를 붙이세요.

언어별 비교 표

언어너비 강제 변환음수 >>BigInt 지원
자바스크립트32비트 부호 있는 정수로 강제; >>>는 부호 없음산술BigInt에 별도 연산자
파이썬임의 정밀도; 고정 너비 없음산술네이티브
Go엄격; 시프트량은 반드시 부호 없는 정수부호 있는 타입은 산술math/big
C/C++타입에 따름; int, unsigned부호 있는 타입은 구현 정의기본 내장 없음
Rust엄격; 디버그 모드에서 오버플로 시 panic부호 있는 타입은 산술u128 / 외부 crate

파이썬의 무한 너비 특성

파이썬 정수는 고정 너비가 없기 때문에, 2의 보수 논리가 왼쪽으로 “무한히” 확장됩니다. ~5250이나 65530이 아니라 -6인 이유도 이것입니다. 파이썬은 결과를 고정 너비 비트 패턴이 아닌 음의 정수로 해석합니다. 래핑 의미가 필요하다면 명시적으로 마스크를 적용하세요.

# Simulate 8-bit NOT
(~5) & 0xFF  # 250

2026년 성능 현실 점검

“비트 연산은 언제나 더 빠르다”는 흔한 속설입니다. 2026년 기준으로는 반만 맞습니다.

컴파일러는 뻔한 변환을 알아서 처리합니다. 최신 옵티마이저는 x * 2를 자동으로 x << 1로 바꿉니다. 속도 때문에 x << 1을 수동으로 쓰는 건 컴파일러를 불신하는 미신입니다. 빨라지지도 않고 가독성만 떨어집니다.

비트 연산 코드가 실제로 효과를 내는 경우는 다음과 같습니다.

  • 수치 연산 핫 루프: popcount, 선행·후행 0 개수 세기, 비트보드 체스 엔진.
  • 공간 효율적인 자료구조: 블룸 필터, 로어링 비트맵(roaring bitmap), 펜윅 트리.
  • 하드웨어 레지스터와 메모리 매핑 I/O: 임베디드 코드, 커널, 펌웨어.
  • 암호학 기본 연산: AES, ChaCha20, SHA는 모두 XOR, 회전, 시프트로 만들어집니다.
  • 압축과 해제: 허프만 코딩, 런 렝스, 패킹된 정수.
  • 데이터베이스 엔진: 비트맵 인덱스, Parquet 사전 인코딩 같은 패킹된 컬럼 형식.

반대로 도움이 안 되는 경우도 있습니다. 요청당 두 번 실행되는 비즈니스 로직에서 x % 2x & 1로 바꾸는 식의 최적화가 그렇습니다. 속도 차이는 측정조차 안 되고 가독성 비용만 남습니다.

비트 조작이 항상 이기는 영역은 메모리입니다. 플래그 32개를 int에 패킹하면 불리언 32개 대비 31바이트를 아낍니다. 규모가 커지면(사용자 레코드 수백만 건, 이벤트 수십억 건) 캐시 친화적인 레이아웃이냐 L2 캐시를 갈아엎는 워크로드냐를 가르는 차이가 됩니다.

빠른 참조 치트 시트

연산연산자예시결과대표 용도
AND&0b1100 & 0b10100b1000비트 마스크/추출
OR|0b1100 | 0b10100b1110플래그 결합
XOR^0b1100 ^ 0b10100b0110토글 / 차이 검출
NOT~~0b1100...11110011마스크용 반전
좌측 시프트<<1 << 382ⁿ 곱셈
우측 시프트>>16 >> 242ⁿ 나눗셈 (부호 있음)
부호 없는 우측 시프트 (JS)>>>-1 >>> 04294967295부호 없음으로 취급
n번 비트 설정|x | (1 << n)비트 켜기
n번 비트 해제& ~x & ~(1 << n)비트 끄기
n번 비트 토글^x ^ (1 << n)비트 뒤집기
n번 비트 확인&(x >> n) & 10 또는 1비트 검사
가장 낮은 켜진 비트& -x & -x비트 분리
2의 거듭제곱 여부&x > 0 && (x & (x-1)) == 0bool거듭제곱 판별

자주 묻는 질문

논리(&&)와 비트(&) AND의 차이는 무엇인가요?

논리 AND는 불리언 값 전체에 대해 동작하며 단락 평가를 합니다. 즉 false && exprexpr을 평가하지 않습니다. 비트 AND는 정수의 개별 비트에 대해 동작하고 항상 양쪽을 모두 평가합니다. 조건문에는 &&를, 비트 조작에는 &를 쓰세요.

왜 대부분의 언어에서 ~1-2와 같은가요?

1에 비트 NOT을 적용하면 모든 비트가 뒤집혀 1의 보수가 만들어집니다. 2의 보수 정수 표현에서 x의 모든 비트를 뒤집으면 -(x + 1)이 되므로, ~1-2, ~0-1, ~(-1)0이 됩니다. 이 항등식은 자바스크립트, 파이썬, Go, C, Rust 등 부호 있는 정수를 2의 보수로 저장하는 모든 언어에서 성립합니다.

x << 1이 정말로 x * 2보다 빠른가요?

아닙니다. 실무에서는 차이가 없습니다. 최신 컴파일러는 x * 2를 인식해 머신 수준에서 같은 시프트 명령을 내보내고, x86이나 ARM에서 측정 가능한 차이가 나지 않습니다. 가독성을 위해 x * 2를 쓰고, <<는 비트 마스크나 구조화된 ID 패킹처럼 의도적으로 비트 단위로 사고할 때만 쓰세요.

자바스크립트는 64비트 비트 연산을 지원하나요?

아닙니다. 자바스크립트는 표준 &, |, ^, <<, >> 연산자로는 64비트 비트 연산을 지원하지 않습니다. 이 연산자들은 실행 전에 피연산자를 32비트 부호 있는 정수로 강제 변환하기 때문입니다. 64비트 이상 값에는 1n << 40n 같은 BigInt 리터럴을 사용하세요. 이 경우 대응되는 별도 연산자와 함께 임의 정밀도의 비트 연산을 수행할 수 있습니다.

켜진 비트 개수를 효율적으로 세려면 어떻게 해야 하나요?

언어에 내장된 기능을 사용하세요. Go에서는 bits.OnesCount, Java에서는 Integer.bitCount, Python 3.10+에서는 .bit_count(), C/C++에서는 popcount 인트린식이 있습니다. 최신 x86과 ARM에서는 단일 POPCNT CPU 명령어로 매핑됩니다.

불리언 구조체 대신 비트 마스크 플래그를 써야 할 때는 언제인가요?

많은 불리언을 압축적으로 저장해야 할 때(데이터베이스, 네트워크 프로토콜, 파일 포맷) 또는 flags & REQUIRED_MASK 같은 단일 AND로 조합을 빠르게 테스트해야 할 때 비트 마스크 플래그를 사용하세요. 필드 타입이 서로 다르거나, 디버깅을 위해 설명적인 출력이 필요하거나, 메모리 몇 바이트보다 가독성이 더 중요할 때는 불리언 구조체를 사용하는 편이 낫습니다.

비트 너비를 초과해서 시프트하면 어떻게 되나요?

C/C++에서는 정의되지 않은 동작(undefined behavior)입니다. 자바스크립트는 시프트량을 mod 32로 취하므로 1 << 320이 아니라 1입니다. 파이썬은 너비가 없으니 1 << 100은 그냥 더 큰 정수가 됩니다. 과도 시프트 동작에 의존하지 말고, 필요하면 시프트량을 직접 마스킹하세요.

파이썬의 ~5가 왜 2가 아니라 -6인가요?

파이썬 정수는 고정 너비가 없으므로 2의 보수가 개념적으로 무한히 확장됩니다. ~5는 다른 모든 2의 보수 언어와 마찬가지로 -(5 + 1) = -6과 같습니다. 8비트 “반전” 값인 250이 필요하다면 마스크를 적용하세요. (~5) & 0xFF.

XOR 암호화는 안전한가요?

메시지와 같은 길이의 무작위 키를 쓰는 일회용 패드(one-time pad)는 정보 이론적으로 해독 불가능합니다. 하지만 메시지 간에 같은 키를 재사용하면 치명적으로 안전하지 않고, 짧은 반복 키를 쓰는 표준 XOR “암호화”는 쉽게 깨집니다. AES나 ChaCha20 같은 실제 암호도 내부적으로 XOR을 쓰지만, 그건 여러 단계 중 하나일 뿐입니다.

2의 보수를 이용해 음수를 손으로 표현하려면 어떻게 하나요?

대상 너비로 양수 값을 2진수로 적고, 비트를 모두 뒤집은 뒤 1을 더하면 됩니다. 예: 8비트에서 -500000101 → 비트 반전 11111010 → 1 더하기 11111011입니다. 진법 변환기에서 251(=11111011의 부호 없는 해석)을 변환해 보면 확인할 수 있습니다.

관련 도구와 더 읽어 보기