[CS300 #089] CPU 데이터패스와 제어 장치 — 명령어 하나가 지나가는 길
컴퓨터공학 300 주제 시리즈의 089번째 글이다. 전체 지도는 여기.
한 줄 요약
데이터패스는 명령어가 실행될 때 데이터가 흘러가는 부품들(PC, 명령어 메모리, 레지스터 파일, ALU, 데이터 메모리, 멀티플렉서)의 배선이고, 제어 장치는 명령어의 연산 코드를 보고 그 배선의 스위치(제어 신호)를 켜고 끄는 회로다. CPU 는 “길” 과 “신호등” 으로 나뉜다.
왜 필요한가
앞 글들에서 게이트, 덧셈기, 레지스터(#084~#085), 저장 프로그램(#086), 명령어 형식(#087)을 따로 봤다. 이 글은 그것들을 하나로 조립한다. 명령어 하나가 실제로 어떤 부품을 어떤 순서로 거치는지 알면 다음이 이해된다.
- 왜 메모리 적재 명령이 덧셈보다 느린가
- 파이프라이닝(#090)이 “어디를” 자르는 것인가
- 해저드(#091)가 왜 생기는가
핵심 개념
구성 부품
| 부품 | 역할 | 종류 |
|---|---|---|
| PC | 다음 명령어 주소 | 순차(레지스터) |
| 명령어 메모리 | PC 주소의 명령어를 내줌 | 메모리 |
| 레지스터 파일 | 32개 레지스터. 읽기 포트 2개, 쓰기 포트 1개 | 순차 |
| 즉시값 생성기 | 명령어에서 상수를 꺼내 부호 확장 | 조합 |
| ALU | 덧셈·뺄셈·논리·비교 | 조합 |
| 데이터 메모리 | 적재·저장 대상 | 메모리 |
| 멀티플렉서(MUX) | 여러 값 중 하나를 고름 | 조합 |
단일 사이클 데이터패스
명령어 하나를 한 클록 사이클에 끝내는 가장 단순한 설계다. RISC-V 의 add, addi, lw, sw, beq 를 처리하는 데이터패스를 단순화하면 다음과 같다.
┌────────────────── +4 ──────────────────┐
│ ▼
┌────┐ │ ┌────────┐ rs1 ┌──────────┐ A ┌─────┐ ┌────────┐
│ PC │───┴─►│명령어 │──────►│ 레지스터 │──►│ │ 주소 │ 데이터 │
└────┘ │메모리 │ rs2 │ 파일 │ B │ ALU │────►│ 메모리 │
▲ └───┬────┘──────►│ │─┐ │ │ └───┬────┘
│ │ rd │ │ ▼ └──┬──┘ │
│ │ ─────►│ 쓰기 ◄──┼─MUX │ 결과 │ 읽은 값
│ ▼ └──────────┘ ▲ ▼ ▼
│ 즉시값 생성 ───────────────────┘ MemToReg MUX ◄──┘
│ │ │
└── PC MUX ◄──┴── (PC + 즉시값, 분기 시) ◄────┘ (ALU 결과 = 0 ?)
그림의 MUX 세 개가 핵심이다.
- ALUSrc MUX: ALU 의 두 번째 입력을 레지스터(rs2)로 할지 즉시값으로 할지.
- MemToReg MUX: 레지스터에 쓸 값을 ALU 결과로 할지 메모리에서 읽은 값으로 할지.
- PC MUX: 다음 PC 를 PC+4 로 할지 분기 목적지로 할지.
제어 장치: 연산 코드 → 제어 신호
제어 장치는 명령어의 opcode(와 funct 필드)를 받아 MUX 선택 신호와 쓰기 허용 신호를 내는 조합 회로다. 단일 사이클에서는 사실상 진리표 하나다.
| 명령 | RegWrite | ALUSrc | MemRead | MemWrite | MemToReg | Branch |
|---|---|---|---|---|---|---|
| add | 1 | 0 | 0 | 0 | 0 | 0 |
| addi | 1 | 1 | 0 | 0 | 0 | 0 |
| lw | 1 | 1 | 1 | 0 | 1 | 0 |
| sw | 0 | 1 | 0 | 1 | x | 0 |
| beq | 0 | 0 | 0 | 0 | x | 1 |
x 는 “아무 값이나 상관없음” 이다. sw, beq 는 레지스터에 쓰지 않으므로 MemToReg 가 무엇이든 결과에 영향이 없다.
명령어별 경로
add x1, x2, x3: 인출 → 레지스터 2개 읽기 → ALU 덧셈 → 레지스터 쓰기. 데이터 메모리는 쓰지 않는다.lw x4, 0(x2): 인출 → x2 읽기 → ALU 로 주소 계산 → 데이터 메모리 읽기 → 레지스터 쓰기. 부품을 가장 많이 거친다.sw x1, 100(x0): 주소 계산 → 메모리 쓰기. 레지스터 쓰기 없음.beq x3, x0, 8: 두 레지스터를 ALU 로 빼서 0 인지 확인 → PC MUX 선택.
단일 사이클의 문제
클록 주기는 가장 느린 명령어(lw)의 경로에 맞춰야 한다. add 는 데이터 메모리를 거치지 않는데도 lw 만큼 기다린다. 해결책은 두 가지다.
- 다중 사이클: 명령어를 여러 단계로 나눠 단계마다 한 사이클씩 쓰고, 명령어마다 필요한 단계 수만 쓴다. 제어 장치가 유한 상태 기계(#085)가 된다.
- 파이프라인: 단계를 나누고 여러 명령어를 겹쳐 실행한다(#090). 현대 CPU 의 표준이다.
하드와이어드 제어와 마이크로프로그램 제어
| 방식 | 구현 | 특징 |
|---|---|---|
| 하드와이어드 | 게이트로 직접 만든 조합 회로·FSM | 빠름, 바꾸기 어려움. RISC 계열에 일반적 |
| 마이크로프로그램 | 제어 신호 묶음(마이크로 명령)을 작은 ROM 에 저장해 순서대로 꺼냄 | 복잡한 명령어를 다루기 쉬움. 전통적 CISC 에서 쓰임 |
현대 x86 CPU 는 흔한 명령어는 하드웨어 해독기로 바로 마이크로 연산으로 바꾸고, 드물고 복잡한 명령어만 마이크로코드 ROM 을 거친다. CPU 제조사가 배포하는 “마이크로코드 업데이트” 가 바로 이 부분을 고치는 것이다.
직접 해 보기
단일 사이클 데이터패스를 파이썬으로 흉내 낸다. 제어 장치는 표 하나이고, 데이터패스는 그 신호로 MUX 를 고른다. 메모리 0~12 번지의 네 수를 더해 100 번지에 저장한다. python3 로 실행해 확인했다.
CONTROL = { # 제어 장치 = 연산 코드 -> 제어 신호 표
# RegWrite ALUSrc MemRead MemWrite MemToReg Branch
"add": (1, 0, 0, 0, 0, 0),
"addi": (1, 1, 0, 0, 0, 0),
"lw": (1, 1, 1, 0, 1, 0),
"sw": (0, 1, 0, 1, 0, 0),
"beq": (0, 0, 0, 0, 0, 1),
}
def step(pc, imem, reg, dmem):
op, rd, rs1, rs2, imm = imem[pc // 4] # 1. 명령어 인출
rw, alusrc, mr, mw, m2r, br = CONTROL[op] # 2. 해독 -> 제어 신호
a, b = reg[rs1], reg[rs2] # 레지스터 파일 읽기
alu_b = imm if alusrc else b # 3. MUX: 즉시값 vs 레지스터
alu = a - alu_b if br else a + alu_b # ALU
mem = dmem.get(alu, 0) if mr else None # 4. 데이터 메모리
if mw: dmem[alu] = b
if rw and rd != 0: # 5. 레지스터 쓰기(x0 은 항상 0)
reg[rd] = mem if m2r else alu
return pc + imm if (br and alu == 0) else pc + 4 # 다음 PC MUX
imem = [
("addi", 2, 0, 0, 0), # 0: x2 = 0 (주소)
("addi", 3, 0, 0, 4), # 4: x3 = 4 (남은 개수)
("lw", 4, 2, 0, 0), # 8: x4 = mem[x2]
("add", 1, 1, 4, 0), # 12: x1 += x4
("addi", 2, 2, 0, 4), # 16: x2 += 4
("addi", 3, 3, 0, -1), # 20: x3 -= 1
("beq", 0, 3, 0, 8), # 24: x3 == 0 이면 32 로
("beq", 0, 0, 0, -20), # 28: 무조건 8 로 (x0 == x0)
("sw", 0, 0, 1, 100), # 32: mem[100] = x1
]
reg = [0] * 8
dmem = {0: 10, 4: 20, 8: 30, 12: 40}
pc, cycles = 0, 0
while pc // 4 < len(imem):
pc = step(pc, imem, reg, dmem); cycles += 1
print("x1 =", reg[1], "mem[100] =", dmem[100], "cycles =", cycles)
출력:
x1 = 100 mem[100] = 100 cycles = 26
명령어 26개를 실행했고 단일 사이클이므로 26 사이클이다. 실제 RISC-V 의 beq 는 ALU 와 별도의 비교기와 주소 덧셈기를 쓰지만, 여기서는 단순화를 위해 ALU 뺄셈 결과로 판정했다. step() 의 다섯 주석이 다음 글에서 볼 5단계 파이프라인의 다섯 단계(IF, ID, EX, MEM, WB)와 그대로 대응한다는 점을 기억해 두자.
현업에서는
- 명령어 비용 감각: 레지스터끼리의 연산은 싸고, 메모리를 거치는 적재는 비싸다. 캐시에 없으면 훨씬 더 비싸다. 핫 루프에서 메모리 접근을 줄이는 최적화가 효과적인 이유다.
- 마이크로코드 업데이트: CPU 보안 취약점(Spectre 계열 등) 대응으로 OS 나 펌웨어가 마이크로코드를 갱신하는 일이 있다. 리눅스에서는 부팅 시 로드되며
/proc/cpuinfo의microcode항목으로 현재 버전을 볼 수 있다. 홈랩 노드마다 이 값이 다르면 같은 워크로드의 성능이 달라질 수 있다. - 하드웨어 설계 교육: Nand to Tetris 나 MIT 6.004 같은 과정은 이 데이터패스를 직접 만들어 보게 한다. 소프트웨어 개발자가 “CPU 가 실제로 무엇을 하는지” 를 몸으로 익히는 가장 빠른 길이다.
확인 문제
- 단일 사이클 데이터패스에서 클록 주기를 정하는 명령어는 보통 무엇이고 왜 그런가.
lw와add에서 MemToReg 신호는 각각 무엇이어야 하는가.- ALUSrc 가 1 인 명령어를 위 표에서 모두 고르라.
- 마이크로프로그램 제어 방식의 장단점은.
풀이
lw. 명령어 메모리, 레지스터 읽기, ALU, 데이터 메모리, 레지스터 쓰기까지 가장 긴 경로를 거치기 때문이다.lw는 1(메모리 값),add는 0(ALU 결과).- addi, lw, sw.
- 복잡한 명령어를 ROM 의 마이크로 명령으로 쉽게 구현·수정할 수 있지만, ROM 을 거치는 만큼 하드와이어드보다 느릴 수 있다.
더 읽을거리 (References)
- RISC-V International, RISC-V Ratified Specifications
- Nand to Tetris — 프로젝트 5: Computer Architecture
- MIT OpenCourseWare, 6.004 Computation Structures (Spring 2017)
- David A. Patterson, John L. Hennessy, Computer Organization and Design RISC-V Edition, Morgan Kaufmann. 4장.