[OS] CPU 구조와 파이프라인 - 4

2026. 3. 11. 11:29운영체제(OS)

CPU 구조와 파이프라인 — Unity 개발자 관점

CPU 구조와 파이프라인 — Unity 개발자 관점

CPU의 세 가지 구성 요소(제어장치, 연산장치, 레지스터)의 원리부터, 파이프라인과 브랜치 예측이 실제로 어떻게 동작하는지, 그리고 Unity 코드에서 어떤 의미를 갖는지를 정리한다.

💡 핵심: CPU 구조를 이해하면 "왜 이 코드가 느린지"를 하드웨어 수준에서 설명할 수 있게 된다. 단순한 최적화 팁이 아니라 원리를 알고 쓰는 최적화가 된다.

1. CPU의 세 가지 구성 요소

CPU는 크게 제어장치(CU), 연산장치(ALU), 레지스터(Register)로 구성된다. 이 셋이 버스1로 연결되어 명령어를 받아 실행하고 결과를 저장하는 전체 흐름을 만든다.

CPU
제어장치 (CU)

명령어 해독, 각 장치에 제어신호 전달

연산장치 (ALU)

산술·논리·비교·시프트 연산 실행

레지스터

CPU 내부 초고속 임시 저장소

제어장치 (Control Unit)

제어장치는 CPU 안에 있는 모든 장치의 동작을 지시하고 제어하는 역할을 한다. IR2에 있는 명령어를 해독기(Decoder)가 해독하면, 그에 맞는 제어신호를 해당 장치로 보낸다. 입력으로는 명령 레지스터, 플래그, 클록 등이 들어온다.

IR (명령어)
Decoder 해독
제어신호 생성
각 장치로 전달

연산장치 (ALU)

제어장치의 명령에 따라 실제 연산을 수행하는 장치다. 산술연산, 논리연산, 관계연산, 시프트(Shift) 등을 처리한다. ALU는 누산기(AC)3, 가산기, 보수기, 시프트 레지스터, 오버플로 검출기 등으로 구성된다.

Unity 코드에서 +, -, &&, ||, >, < 같은 연산이 실제로 ALU를 거친다. Vector3의 사칙연산도 마찬가지다. 현대 CPU는 ALU를 여러 개 두고 비순서 실행4으로 명령어를 재배치해 병렬 처리한다.

레지스터 (Register)

CPU 내부에 있는 가장 빠른 저장소다. 플립플롭5으로 구성되어 있으며, 명령어나 연산의 중간 결과값을 임시로 기억한다. 메모리보다 압도적으로 빠른 대신 용량이 극히 작다. 주요 레지스터는 아래와 같다.

레지스터역할
PC (Program Counter)다음에 실행할 명령어의 주소를 저장. 분기 발생 시 이 값이 바뀐다.
IR (Instruction Register)현재 실행 중인 명령어의 내용을 보관
MAR메모리 접근 시 주소를 임시 저장
MBR메모리에서 읽어온 데이터 임시 보관. CPU가 처리하기 위해 반드시 여기 거침.
AC (Accumulator)연산 중간 결과 저장. ALU 출력의 종착지.
Flag Register오버플로, 캐리, 부호, 제로 등 연산 상태 비트

2. 교재 속 CPU와 현대 CPU의 차이

교재에서 설명하는 CPU 구조는 개념을 이해하기 위한 단순화된 모델이다. 현대 CPU는 같은 원리 위에서 훨씬 복잡하게 발전했다.

레지스터 수의 폭발적 증가

교재는 PC, IR, MAR, MBR, AC 등 몇 개의 핵심 레지스터를 설명하지만, 현대 x86-64 CPU는 범용 레지스터만 16개(RAX~R15)이고, 128비트 XMM 레지스터 16개, 256비트 YMM 레지스터, AVX-512를 지원하면 512비트 ZMM 레지스터 32개까지 존재한다. 레지스터가 많을수록 스택에 값을 넘기는 빈도가 줄어 성능이 높아진다.

RAX
RBX
RCX
RDX
RSI
RDI
RSP
RBP
R8
R9
R10
R11
R12
R13
R14
R15
XMM0~15
YMM0~15
ZMM0~31

파이프라인의 세분화

교재의 CPU는 명령어를 순차적으로 처리하는 구조를 전제한다. 현대 CPU는 Fetch→Decode→Execute→Writeback을 겹쳐서 동시에 처리하는 파이프라인 구조를 쓴다. Intel Skylake6 기준으로 이 과정이 14단계로 세분화되어 있다. 분기 예측 실패 시 패널티가 약 15~17사이클인 것도 이 깊이 때문이다.

캐시 계층의 등장

교재에서 레지스터 다음은 바로 메인 메모리다. 현대 CPU에는 그 사이에 L1/L2/L3 캐시 계층이 존재한다. CPU 성능의 상당 부분이 이 캐시 적중률에 달려있으며, 메인 메모리 접근은 캐시 대비 수십~수백 배 느리다.

L1 캐시
~5사이클
L2 캐시
~12사이클
L3 캐시
~40사이클
메인 메모리
100사이클 이상

멀티코어와 인터커넥트

교재는 단일 CPU 기준이다. 현대 CPU는 코어가 수십 개이고, 이 코어들이 Ring Bus나 Mesh 같은 내부 인터커넥트로 연결된다. L3 캐시는 모든 코어가 공유하며, 코어 간 캐시 일관성을 유지하는 것 자체가 별도의 복잡한 하드웨어 로직이다.

항목교재 기준현대 CPU (x86-64)
범용 레지스터소수 (PC, IR, AC 등)16개 + SIMD 레지스터 수십 개
파이프라인순차 처리 모델14~19단계 (Skylake 기준 14단계)
메모리 계층레지스터 → 메인 메모리레지스터 → L1 → L2 → L3 → RAM
코어 수단일 코어수십 코어, 멀티스레드
명령어 실행 순서순차 실행비순서 실행(Out-of-Order)

3. 파이프라인7과 브랜치 예측8

파이프라인 구조에서 CPU는 공장 컨베이어벨트처럼 여러 명령어를 단계별로 겹쳐 처리한다. 명령어 A가 Decode 단계일 때, 명령어 B는 Fetch 단계에 이미 올라와 있다.

Fetch
Decode
Execute
Writeback

문제는 if문(분기 명령어)이다. CPU가 분기 명령어를 Fetch하는 시점에, 그 조건값은 아직 Execute 단계에서 계산 중이다. 파이프라인을 멈출 수 없으니 CPU는 결과를 예측하고 해당 경로의 명령어를 미리 파이프라인에 채운다.

예측 성공

if (isAlive)
예측: true
✅ 그대로 실행

예측 실패 → 파이프라인 플러시9

if (hp < 20f)
예측: false
실제: true!
⚠️ 플러시 + 재시작
사이클 1234567
if 명령어 FetchDecodeExecWrite ---
예측 경로 (틀림) -FetchDecode ❌ 폐기---
실제 경로 ---- FetchDecodeExec

파이프라인 중간에 올라와 있던 명령어가 전부 날아가고 올바른 경로를 처음부터 시작한다. Skylake 기준으로 이 패널티가 약 15~17사이클이다.

💡 중요: 브랜치 예측기10는 패턴을 학습한다. 문제는 if문 개수가 아니라 결과의 예측 가능성이다. 항상 true인 분기는 사실상 비용이 없다.
if문패턴예측 난이도
if (isAlive) (거의 항상 true)true × ∞✅ 매우 쉬움
if (timer >= 0.2f)false × 12 → true × 1 반복✅ 쉬움
if (hp < 20f) (전투 중 불규칙)불규칙❌ 어려움
if (Random.value > 0.5f)완전 랜덤❌ 불가능

4. Unity 개발자가 알면 도움되는 것들

캐시 지역성 — NativeArray와 DOTS

CPU가 메모리를 읽을 때 캐시에 없으면(캐시 미스) 100사이클 이상을 대기한다. 이 때 CPU는 캐시라인11 단위(x86-64 기준 64바이트)로 메모리를 통째로 가져온다. 따라서 연속된 메모리를 순서대로 읽으면 한 번 가져온 캐시라인을 계속 재사용하지만, 여기저기 흩어진 메모리를 무작위로 읽으면 매번 캐시 미스가 발생한다.

❌ 캐시 미스 잦음
List<Enemy> enemies;

// 각 Enemy 객체가 힙 여기저기 흩어짐
foreach (var e in enemies)
  e.Update(); // 매번 캐시 미스
✅ 캐시 친화적
NativeArray<EnemyData> data;

// 연속 메모리에 데이터 밀집
for (int i = 0; i < data.Length; i++)
  Process(data[i]); // 캐시 히트

DOTS/ECS가 성능이 좋은 핵심 이유 중 하나가 바로 이것이다. 같은 컴포넌트를 가진 엔티티를 메모리에 연속으로 배치해서, 캐시 미스를 구조적으로 줄인다.

레지스터 부족 → 스택 프레임과 StackOverflow

레지스터 수는 한정돼 있다. 레지스터가 부족하면 CPU는 값을 스택 메모리에 저장한다. C#에서 메서드를 호출할 때마다 스택 프레임이 쌓이는데, 재귀 호출이 깊어지면 이 스택이 한계를 초과해 StackOverflowException이 발생한다. 교재에서 배운 "레지스터 수가 제한적"이라는 개념이 직접 연결되는 지점이다.

버스 대역폭 — DrawCall과 GPU 전송

버스는 CPU, 메모리, I/O 장치 사이에 데이터를 실어 나르는 공용 전송선이다. CPU↔GPU 간 데이터 전달도 PCIe 버스 대역폭에 제한을 받는다. Mesh, Texture를 매 프레임 CPU에서 GPU로 전송하는 구조는 이 버스를 반복적으로 점유한다.

CPU (매 프레임 데이터 준비)
PCIe 버스
GPU

Static Batching과 GPU Instancing12이 성능을 높이는 이유가 바로 이 전송 횟수를 줄이기 때문이다. 같은 Mesh를 여러 오브젝트가 쓸 때 GPU Instancing을 쓰면 메시 데이터를 한 번만 전송하고, 인스턴스마다 다른 변환 행렬만 별도로 넘긴다.

ALU와 float 연산 — sqrMagnitude

ALU는 정수 연산을 기본으로 하고, 부동소수점 연산은 FPU13가 담당한다. Vector3.Distance()는 내부적으로 제곱근(Sqrt)을 호출하는데, 이는 일반 사칙연산보다 훨씬 비싼 연산이다. 단순히 거리를 비교할 목적이라면 제곱끼리 비교해도 결과가 같기 때문에 Sqrt를 생략할 수 있다.

❌ Sqrt 호출 포함
float dist = Vector3.Distance(
  a.position, b.position
);
if (dist < range) Chase();
✅ Sqrt 없음
float sqrDist = (a.position
  - b.position).sqrMagnitude;
if (sqrDist < range * range)
  Chase();

Update() 불규칙 분기 → 상태 머신

Update()에서 결과가 매 프레임 불규칙하게 바뀌는 분기가 많으면, 브랜치 예측기가 계속 실패하면서 파이프라인 플러시가 쌓인다. Enemy 100마리 × 분기 4개 × 60fps = 프레임당 24,000번의 예측 시도다. 상태 머신은 이 문제를 구조적으로 해결한다. 조건 체크를 이벤트 발생 시점에만 한 번 수행하고, Update는 현재 상태의 로직만 분기 없이 실행한다.

// ❌ 매 프레임 불규칙 분기
void Update() {
    if (hp < 20f)       PlayLowHpAnim(); // 전투 중 불규칙
    if (target != null) ChaseTarget();   // 플레이어 이동으로 불규칙
    if (isStunned)       return;          // 랜덤 타이밍
    if (isAttacking)     ApplyDamage();   // 공격 타이밍 불규칙
}

// ✅ 상태 머신 — Update에서는 분기 없이 현재 상태만 실행
void Update() => ExecuteCurrentState();

public void OnDamaged(float damage) {
    hp -= damage;
    if (hp < 20f) ChangeState(EnemyState.LowHp); // 이벤트 발생 시 한 번만
}
💡 정리: "if문이 많으면 느리다"가 아니라 "예측 불가능한 분기가 많으면 느리다"가 정확한 표현이다. 타이머 체크처럼 패턴이 규칙적인 분기는 예측기가 학습해서 거의 실패하지 않는다.

📎 용어 설명

  1. 버스(Bus) — CPU, 메모리, I/O 장치 간에 데이터를 실어 나르는 공용 전송선. 제어 버스(단방향), 주소 버스(단방향), 데이터 버스(양방향) 세 종류가 있다.
  2. IR (Instruction Register) — 현재 실행 중인 명령어의 내용을 담고 있는 레지스터. 제어장치가 이 값을 읽어 해독한다.
  3. AC (Accumulator, 누산기) — 연산 결과를 임시로 저장하는 레지스터. ALU 출력이 먼저 여기 쌓인 뒤 다음 연산으로 이어진다.
  4. 비순서 실행(Out-of-Order Execution) — CPU가 명령어의 데이터 의존성을 분석해, 순서에 무관하게 준비된 명령어를 먼저 실행하는 기법. 파이프라인 효율을 높인다.
  5. 플립플롭(Flip-Flop) — 1비트를 저장할 수 있는 디지털 회로. 레지스터는 플립플롭을 병렬로 연결해 구성한다.
  6. Skylake — 2015년 출시된 Intel 6세대 Core 마이크로아키텍처. 파이프라인 깊이 14단계, 분기 예측 실패 패널티 약 15~17사이클.
  7. 파이프라인(Pipeline) — 명령어를 Fetch→Decode→Execute→Writeback 단계로 나눠 여러 명령어를 동시에 처리하는 CPU 구조.
  8. 브랜치 예측(Branch Prediction) — CPU가 분기 명령어의 결과를 미리 예측해 파이프라인을 채우는 기법.
  9. 파이프라인 플러시(Pipeline Flush) — 분기 예측 실패 시 파이프라인에 올라와 있던 잘못된 명령어를 전부 버리고 처음부터 다시 채우는 작업.
  10. 브랜치 예측기(Branch Predictor) — CPU 내부에서 분기 패턴을 학습해 다음 분기 결과를 예측하는 하드웨어 유닛.
  11. 캐시라인(Cache Line) — CPU가 캐시와 메모리 사이에서 데이터를 주고받는 최소 단위. x86-64 기준 64바이트. 1바이트만 필요해도 64바이트를 통째로 가져온다.
  12. GPU Instancing — 같은 Mesh를 여러 오브젝트가 공유할 때, 메시 데이터는 한 번만 GPU에 전송하고 각 오브젝트의 변환 행렬만 별도로 넘겨 DrawCall을 줄이는 기법.
  13. FPU (Floating Point Unit) — CPU 내부에서 부동소수점 연산을 전담하는 유닛. C#의 float 연산이 이 유닛을 거친다. 현대 CPU에서는 SIMD 레지스터(XMM/YMM)를 통한 벡터 연산도 지원한다.
CPU 파이프라인 브랜치 예측 운영체제 Unity 최적화 C# 캐시 상태 머신 ALU