CPU 구조: 명령어 처리 파이프라인과 캐시 계층으로 성능을 끌어올리는 구조
CPU는 산술논리연산장치(ALU), 제어장치(CU), 레지스터, 캐시로 구성되어 인출-해독-실행 사이클을 반복하며 명령어를 처리한다. 이 사이클을 여러 단계로 쪼개 동시에 처리하는 파이프라이닝(pipelining)과, 느린 메인 메모리 접근을 줄이는 다단계 캐시 구조가 현대 CPU 성능의 핵심이다. 이 글에서는 CPU의 내부 구조와 명령어 처리 원리, 그리
1. 개념
CPU(Central Processing Unit)는 명령어를 인출(fetch)하고 해독(decode)하고 실행(execute)하는 하드웨어 장치다. 내부는 크게 네 가지 요소로 구성된다.
- ALU(Arithmetic Logic Unit): 덧셈, 뺄셈, 비교, 논리 연산 등 실제 연산을 수행하는 회로
- CU(Control Unit): 명령어를 해독하고 각 장치에 제어 신호를 보내는 제어 장치
- 레지스터(Register): CPU 내부에 있는 초고속 저장 공간. 프로그램 카운터(PC), 명령어 레지스터(IR), 범용 레지스터 등이 있다
- 캐시(Cache): 메인 메모리보다 빠르지만 용량이 작은 저장 공간. L1, L2, L3 계층으로 나뉜다
이 구성 요소들이 시스템 버스(주소 버스, 데이터 버스, 제어 버스)를 통해 메모리 및 입출력 장치와 통신한다.
2. 왜 사용하는가
CPU 구조가 이렇게 설계된 이유는 근본적으로 속도 격차 문제를 해결하기 위해서다. 레지스터 접근은 1클럭 이내로 끝나지만 메인 메모리(DRAM) 접근은 수백 클럭이 걸린다. 이 격차를 메우기 위해 캐시라는 중간 계층을 두고, 자주 쓰는 데이터를 CPU 가까이에 미리 가져다 놓는다.
또한 명령어 하나를 처리하는 데 여러 단계(인출→해독→실행→메모리 접근→기록)가 필요한데, 이를 순차적으로 처리하면 각 단계의 하드웨어가 대부분 놀게 된다. 파이프라이닝은 이 단계들을 조립 라인처럼 겹쳐 실행해 처리량(throughput)을 높인다.
3. 동작 원리
3.1 명령어 사이클 (Fetch-Decode-Execute)
- 인출(Fetch): PC가 가리키는 주소에서 명령어를 메모리로부터 읽어 IR에 저장하고 PC를 증가시킨다
- 해독(Decode): CU가 명령어를 해석해 어떤 연산인지, 어떤 레지스터/메모리를 쓰는지 판단한다
- 실행(Execute): ALU가 실제 연산을 수행한다
- 메모리 접근(Memory Access): 필요하면 메모리에서 읽거나 쓴다
- 기록(Write Back): 결과를 레지스터에 저장한다
3.2 파이프라이닝
위 5단계를 각각 별도의 하드웨어 유닛으로 만들면, 명령어 A가 실행 단계에 있을 때 명령어 B는 해독 단계, 명령어 C는 인출 단계를 동시에 진행할 수 있다. 이상적으로는 5단계 파이프라인에서 처리량이 5배 가까이 늘어나지만, 실제로는 아래 두 가지 문제(해저드, hazard) 때문에 이론치에 못 미친다.
- 데이터 해저드(Data Hazard): 다음 명령어가 이전 명령어의 결과를 필요로 할 때 대기해야 함
- 제어 해저드(Control Hazard): 분기(branch) 명령어의 결과가 나오기 전까지 다음에 인출할 명령어를 확정할 수 없음
이를 완화하기 위해 분기 예측(Branch Prediction)과 비순차 실행(Out-of-Order Execution) 같은 기법이 쓰인다.
3.3 캐시 계층과 지역성
캐시는 프로그램의 두 가지 지역성(locality)을 활용한다.
- 시간 지역성(Temporal Locality): 최근에 접근한 데이터는 곧 다시 접근될 가능성이 높다
- 공간 지역성(Spatial Locality): 특정 주소에 접근하면 그 근처 주소도 곧 접근될 가능성이 높다
캐시는 데이터를 캐시 라인(보통 64바이트) 단위로 가져오기 때문에, 배열을 순차적으로 접근하는 코드는 캐시 히트율이 높고, 무작위로 접근하는 코드는 캐시 미스가 잦다.
4. 코드 예제
2차원 배열을 행 우선(row-major)으로 순회할 때와 열 우선(column-major)으로 순회할 때의 속도 차이를 통해 캐시 지역성의 효과를 확인할 수 있다.
import time
N = 2000
matrix = [[i + j for j in range(N)] for i in range(N)]
# 행 우선 순회: 메모리에 저장된 순서와 일치 (공간 지역성이 좋음)
start = time.perf_counter()
total = 0
for i in range(N):
for j in range(N):
total += matrix[i][j]
row_major_time = time.perf_counter() - start
# 열 우선 순회: 메모리 저장 순서와 어긋남 (공간 지역성이 나쁨)
start = time.perf_counter()
total = 0
for j in range(N):
for i in range(N):
total += matrix[i][j]
col_major_time = time.perf_counter() - start
print(f"행 우선 순회: {row_major_time:.4f}초")
print(f"열 우선 순회: {col_major_time:.4f}초")
print(f"배율: {col_major_time / row_major_time:.2f}배")파이썬 리스트는 C 배열처럼 완전히 연속된 메모리는 아니지만(포인터 배열), C나 Java의 순수 2차원 배열, 혹은 NumPy 배열로 같은 실험을 하면 캐시 미스로 인한 차이가 훨씬 뚜렷하게 나타난다. 아래는 개념을 보여주는 의사 원리를 C 스타일로 표현한 것이다.
# NumPy를 쓰면 실제 연속 메모리 배열에서 차이를 더 명확히 볼 수 있다
import numpy as np
import time
N = 4000
arr = np.arange(N * N, dtype=np.int64).reshape(N, N)
start = time.perf_counter()
total = np.sum(arr, axis=1) # 행 방향 순회, 캐시 친화적
print("행 우선(axis=1):", time.perf_counter() - start)
start = time.perf_counter()
total = np.sum(arr, axis=0) # 열 방향 순회, 캐시 비친화적
print("열 우선(axis=0):", time.perf_counter() - start)5. 시간 복잡도 또는 성능 특성
메모리 계층별 접근 지연 시간은 대략 다음 규모로 알려져 있다(CPU 세대와 클럭에 따라 달라질 수 있는 근사치).
| 계층 | 대략적인 지연 시간 | 대략적인 클럭 사이클 | |---|---|---| | 레지스터 | 1클럭 이내 | 0~1 | | L1 캐시 | 약 1ns 내외 | 약 4 | | L2 캐시 | 약 3~5ns | 약 10~12 | | L3 캐시 | 약 10~20ns | 약 30~40 | | 메인 메모리(DRAM) | 약 60~100ns | 약 100~300 |
L1과 메인 메모리 사이의 지연 차이는 수십~백 배 수준이다. 이 때문에 알고리즘의 이론적 시간 복잡도가 같더라도, 캐시 히트율이 높은 코드가 실제 실행 시간에서 크게 앞설 수 있다.
파이프라이닝의 이론적 처리량 향상은 파이프라인 단계 수에 비례하지만, 분기 예측 실패 시 파이프라인을 비우는(pipeline flush) 비용이 발생해 실제 향상 폭은 이보다 작다.
6. 실무 사용 사례
- 캐시 친화적 자료구조 설계: 연결 리스트(linked list)보다 배열(array) 기반 구조가 순회 성능에서 유리한 경우가 많다. 포인터를 따라가는 연결 리스트는 각 노드가 메모리 곳곳에 흩어져 있어 캐시 미스가 잦다
- False Sharing 회피: 멀티스레드 환경에서 서로 다른 스레드가 같은 캐시 라인에 속한 변수를 각각 수정하면, 실제로는 데이터 경합이 없어도 캐시 일관성 프로토콜 때문에 성능이 크게 떨어진다. 구조체 필드를 패딩(padding)해 캐시 라인을 분리하는 방식으로 해결한다
- 분기 예측을 고려한 코드 작성: 정렬된 데이터에 대한 조건 분기는 예측 성공률이 높아 정렬되지 않은 데이터보다 빠르게 처리되는 경우가 실측으로 보고된다
- SIMD와 벡터화: 컴파일러나 NumPy 같은 라이브러리가 여러 데이터를 한 명령어로 처리하는 SIMD(Single Instruction Multiple Data) 명령어를 활용해 반복 연산을 가속한다
7. 주의할 점
- 시간 복잡도가 동일한 두 알고리즘도 캐시 지역성 차이로 실제 실행 시간이 수 배 차이 날 수 있다. 빅오(Big-O) 분석만으로 실무 성능을 단정하면 안 된다
- 멀티코어 환경에서 캐시는 코어마다 L1/L2가 따로 있고 L3를 공유하는 구조가 일반적이다. 이 때문에 코어 간 데이터 공유 시 캐시 일관성 프로토콜(예: MESI) 비용이 발생한다
- 최신 CPU는 비순차 실행, 추측 실행(speculative execution) 같은 최적화를 쓰는데, 이 과정에서 Spectre, Meltdown 같은 사이드 채널 취약점이 드러난 바 있다. 성능 최적화 기법이 보안과 상충할 수 있다는 점을 인지해야 한다
- 벤치마크로 캐시 효과를 측정할 때는 JIT 워밍업, OS 스케줄링, 다른 프로세스의 캐시 오염 등 변수를 통제해야 신뢰할 수 있는 수치를 얻는다
8. 핵심 정리
CPU는 ALU, CU, 레지스터, 캐시로 구성되며 인출-해독-실행 사이클을 반복해 명령어를 처리한다. 파이프라이닝은 이 사이클의 각 단계를 겹쳐 실행해 처리량을 높이지만 데이터 해저드와 제어 해저드로 인해 이론치에는 못 미친다. 캐시는 메인 메모리와의 속도 격차를 메우기 위해 시간 지역성과 공간 지역성을 활용하며, 개발자가 이를 고려해 자료구조와 순회 방식을 설계하면 동일한 로직도 실질적인 성능 차이를 만들어낼 수 있다.