본문 바로가기
TYLER SONG2026
블로그 목록
CS학습

Register: CPU 내부의 가장 빠른 저장 공간

레지스터는 CPU 내부에 위치한 소규모 고속 저장 장치로, 산술 연산과 명령어 실행에 직접 사용되는 데이터를 담는다. 메모리 계층 구조의 최상단에 위치하며 접근 속도가 사실상 클럭 사이클 단위로 측정될 만큼 빠르다. 이 글에서는 레지스터의 종류, 컴파일러가 레지스터를 할당하는 원리, 실제 어셈블리 코드에서 레지스터가 어떻게 쓰이는지를 다룬다.

송민성7분 읽기

1. 개념

레지스터(register)는 CPU 다이(die) 내부에 물리적으로 존재하는 저장 회로다. 플립플롭(flip-flop) 또는 래치(latch)로 구성되며, 하나의 레지스터는 보통 32비트 또는 64비트 크기의 값을 담는다.

메모리 계층 구조에서 레지스터는 캐시(cache)보다도 위에 있다.

text
레지스터 → L1 캐시 → L2 캐시 → L3 캐시 → 메인 메모리 → 디스크 (가장 빠름, 가장 작음) (가장 느림, 가장 큼)

x86-64 아키텍처는 범용 레지스터(general-purpose register) 16개(RAX, RBX, RCX, RDX, RSI, RDI, RBP, RSP, R8~R15)를 제공한다. 이 외에도 특수 목적 레지스터인 프로그램 카운터(PC, x86에서는 RIP), 스택 포인터(SP), 플래그 레지스터(FLAGS) 등이 있다.

2. 왜 사용하는가

ALU(산술논리연산장치)는 메모리에서 직접 값을 읽어 연산하지 않는다. 반드시 레지스터에 값을 적재(load)한 뒤 연산하고, 결과를 다시 레지스터에 저장한다. 이는 폰 노이만 구조와 하버드 구조 모두에서 공통된 제약이며, RISC 아키텍처에서는 이를 "load-store 아키텍처"라고 부른다.

메인 메모리 접근에는 수십~수백 클럭 사이클이 걸리지만 레지스터 접근은 1클럭 사이클 이내(파이프라인에 따라 사실상 지연 없음)로 처리된다. 레지스터가 없다면 모든 연산마다 메모리 왕복이 발생해 CPU 성능이 수백 배 저하된다.

3. 동작 원리

레지스터는 레지스터 파일(register file)이라는 작은 배열 형태로 CPU 내부에 배치되고, 디코더(decoder)를 통해 명령어 오퍼랜드가 어느 레지스터를 가리키는지 지정한다.

명령어 실행 파이프라인에서 레지스터는 다음 단계마다 관여한다.

text
Fetch(인출) → Decode(해독) → Execute(실행) → Memory(접근) → Writeback(기록)
  • Decode 단계에서 레지스터 번호를 읽어 레지스터 파일에서 값을 꺼낸다.
  • Execute 단계에서 ALU가 레지스터 값으로 연산한다.
  • Writeback 단계에서 결과를 다시 레지스터에 쓴다.

레지스터 수는 명령어 인코딩 비트 수에 의해 제한된다. x86-64는 오퍼랜드당 4비트를 사용해 16개 레지스터를 표현하고(REX 프리픽스로 확장), ARM64는 5비트를 사용해 31개의 범용 레지스터를 표현한다.

컴파일러는 레지스터 할당(register allocation)이라는 최적화 단계에서, 변수를 가능한 한 레지스터에 유지하고 레지스터가 부족하면 스택(메모리)으로 밀어내는 스필링(spilling)을 수행한다. 이 문제는 그래프 색칠(graph coloring) 알고리즘으로 모델링된다.

4. 코드 예제

C 코드가 어셈블리로 컴파일될 때 변수가 레지스터에 어떻게 매핑되는지 확인한다.

shell
# add.c 컴파일 후 어셈블리 생성 cat > add.c << 'EOF' int add(int a, int b) { int result = a + b; return result; } EOF gcc -O2 -S -masm=att add.c -o add.s cat add.s

생성된 어셈블리(x86-64, System V ABI 기준, 최적화 -O2 적용)는 다음과 유사하다.

text
add: leal (%rdi,%rsi), %eax # eax = a(rdi) + b(rsi) ret

인자 a, b는 각각 EDI, ESI 레지스터로 전달되고, 반환값은 EAX 레지스터에 담긴다. 최적화가 적용되면 지역 변수 result는 메모리에 저장되지 않고 레지스터 안에서만 존재한다.

레지스터 부족으로 스필링이 발생하는 경우를 보여주는 예제다.

c
// 레지스터가 부족할 만큼 변수가 많으면 스택 사용이 늘어난다 long compute(long a, long b, long c, long d, long e, long f, long g, long h, long i, long j) { return a + b + c + d + e + f + g + h + i + j; }
shell
gcc -O2 -S -masm=att spill.c -o spill.s

변수 10개 중 처음 6개(a~f)는 x86-64 System V 호출 규약(calling convention)에 따라 RDI, RSI, RDX, RCX, R8, R9 레지스터로 전달되지만, 7번째부터는 스택을 통해 전달된다. 이는 레지스터 자원이 물리적으로 유한하다는 사실을 직접 보여준다.

register 키워드를 사용한 옛 C 코드 스타일도 참고할 수 있다.

c
// C89 스타일: 컴파일러에게 레지스터 사용을 "힌트"로만 제공 // 현대 컴파일러(GCC, Clang)는 이 키워드를 대부분 무시하고 // 자체 레지스터 할당 알고리즘으로 판단한다 int sum_loop(int n) { register int i; register int total = 0; for (i = 0; i < n; i++) { total += i; } return total; }

5. 시간 복잡도 또는 성능 특성

정확한 사이클 수는 마이크로아키텍처마다 다르지만, 일반적으로 알려진 상대적 지연시간(latency)은 다음과 같다.

| 저장 위치 | 대략적인 접근 지연시간 | |---|---| | 레지스터 | 0~1 클럭 사이클 | | L1 캐시 | 약 4클럭 사이클 (약 1ns) | | L2 캐시 | 약 10~12클럭 사이클 | | L3 캐시 | 약 30~40클럭 사이클 | | 메인 메모리(DRAM) | 약 100ns 이상 (수백 클럭 사이클) |

이 수치는 Intel/AMD 최신 x86-64 프로세서를 기준으로 한 대략적인 값이며, 실제 값은 클럭 속도, 캐시 구조, 메모리 컨트롤러 성능에 따라 달라진다.

6. 실무 사용 사례

함수 호출 규약(calling convention): x86-64 System V ABI는 함수 인자 전달에 RDI, RSI, RDX, RCX, R8, R9 레지스터를 순서대로 사용한다. 디버거로 스택 프레임을 분석하거나 크래시 덤프를 읽을 때 이 규약을 알아야 인자값을 추적할 수 있다.

성능 최적화: 반복문 내부 변수를 레지스터에 유지시키기 위해 컴파일러 최적화 플래그(-O2, -O3)를 사용한다. 루프 안에서 배열 인덱스 대신 포인터 연산을 쓰거나 변수 개수를 줄이면 레지스터 스필링을 줄일 수 있다.

임베디드/드라이버 개발: 메모리 매핑된 I/O 레지스터(memory-mapped register)는 CPU 레지스터와는 다른 개념이지만, 하드웨어 제어 레지스터에 직접 값을 쓰는 저수준 코드에서 volatile 키워드로 컴파일러가 레지스터 캐싱 최적화를 하지 못하게 막아야 한다.

디버깅: gdb에서 info registers 명령으로 현재 CPU 레지스터 상태를 확인해 세그멘테이션 폴트 발생 시점의 레지스터 값을 분석한다.

shell
gdb ./a.out (gdb) break main (gdb) run (gdb) info registers

7. 주의할 점

  • C의 register 키워드는 힌트일 뿐 강제가 아니다. 현대 컴파일러는 자체 레지스터 할당 알고리즘이 훨씬 정교하므로 이 키워드에 의존하지 않는 게 좋다. C++17부터는 이 키워드가 사실상 사용 중단(deprecated) 상태다.
  • volatileregister는 반대 개념에 가깝다. volatile은 매번 메모리에서 다시 읽으라는 지시이고, register는 레지스터에 유지하라는 지시다. 하드웨어 제어 레지스터를 다룰 때 register를 쓰면 안 된다.
  • 레지스터 개수는 아키텍처마다 다르다. 크로스 플랫폼 어셈블리나 저수준 코드를 작성할 때 x86-64와 ARM64의 레지스터 명명 규칙과 개수 차이를 혼동하지 않아야 한다.
  • 멀티스레드 환경에서 레지스터는 스레드마다 독립적인 문맥(context)으로 저장된다. 컨텍스트 스위치(context switch) 시 레지스터 값 전체를 저장하고 복원하는 비용이 발생하며, 이는 스레드 전환 오버헤드의 주요 원인 중 하나다.

8. 핵심 정리

레지스터는 CPU 내부에서 ALU가 직접 접근할 수 있는 가장 빠른 저장 공간이며, 개수가 명령어 인코딩 비트 수에 의해 물리적으로 제한된다. 컴파일러는 레지스터 할당과 스필링을 통해 제한된 레지스터 자원을 최적으로 배분하며, 함수 호출 규약은 어떤 레지스터가 인자와 반환값 전달에 쓰이는지를 표준화한다. 저수준 디버깅이나 성능 최적화 작업을 할 때 레지스터 구조를 이해하면 어셈블리 코드와 컴파일러 최적화 결과를 정확히 해석할 수 있다.

© 2026 Tyler Song