시리즈 로드맵
| # | 주제 | 상태 |
|---|---|---|
| 1 | RISC-V란 무엇인가 | ✅ |
| 2 | Instruction이란: 인코딩과 포맷 | ✅ |
| 3 | 파이프라인의 거시 구조와 하드웨어 유닛 | ✅ |
| 4 | 파이프라인 심화: 해저드 처리와 분기 예측 | 🔲 |
| 5 | 요약, 심화 아키텍처, 다른 CPU 아키텍처 | 🔲 |
한 명령어를 처리하는 다섯 가지 일
2편에서 명령어는 32비트 숫자이고, 디코딩으로 필드를 뽑아낸다고 했다. 그런데 명령어를 실제로 실행하려면 디코딩만으로는 부족하다. 하나의 명령어를 끝내기까지 CPU는 서로 다른 다섯 가지 일을 순서대로 해야 한다.
lw x5, 8(x6)(메모리에서 값을 읽어 x5에 넣기)을 예로 들면 이렇다.
- 명령어 자체를 메모리에서 가져온다.
- 명령어를 해석하고, 필요한 레지스터(
x6)를 읽는다. - 주소를 계산한다(
x6 + 8). - 그 주소의 데이터 메모리를 읽는다.
- 읽은 값을 레지스터(
x5)에 쓴다.
이 다섯 가지 일이 RISC-V 고전 파이프라인의 다섯 단계다.
| 단계 | 이름 | 하는 일 |
|---|---|---|
IF |
Instruction Fetch | 명령어를 메모리에서 가져옴 |
ID |
Instruction Decode | 명령어 해석 + 레지스터 읽기 |
EX |
Execute | ALU 연산 (계산, 주소 계산) |
MEM |
Memory | 데이터 메모리 접근 (load/store) |
WB |
Write Back | 결과를 레지스터에 쓰기 |
이 글은 각 단계가 무엇을 하고 어떤 하드웨어 유닛이 그것을 담당하는지를 거시적으로 조망한다. 단계 사이의 충돌(해저드) 문제는 4편에서 다룬다.
왜 다섯 단계로 나누는가
가장 단순한 CPU는 명령어 하나를 한 클럭에 처음부터 끝까지 처리한다. 이를 단일 사이클(single-cycle) 방식이라 한다. 문제는 클럭 주기다. 클럭 하나가 위 다섯 일을 전부 담을 만큼 길어야 한다. 가장 느린 명령어에 클럭을 맞춰야 하므로 클럭 속도가 낮아진다.
파이프라인은 이 다섯 일을 단계로 쪼개고, 각 단계에 서로 다른 명령어를 동시에 올린다. 공장의 조립 라인과 같다. 한 제품이 모든 공정을 마칠 때까지 기다리지 않고, 각 공정에 서로 다른 제품이 흐른다.

명령어 하나를 끝내는 데 여전히 다섯 클럭이 걸린다(레이턴시는 그대로다). 그러나 정상 상태에서는 매 클럭 하나씩 명령어가 완료된다. 처리량(throughput)이 다섯 배가 된다. 클럭 주기도 다섯 일 중 가장 긴 하나에만 맞추면 되므로 클럭 속도를 높일 수 있다.
파이프라인의 이득은 레이턴시가 아니라 처리량이다. 이 구분이 파이프라인을 이해하는 핵심이다.
데이터패스: 다섯 단계와 하드웨어 유닛
각 단계는 전담 하드웨어 유닛을 가진다. 단계마다 다른 유닛을 쓰기 때문에 다섯 명령어가 겹쳐도 자원이 충돌하지 않는다.

IF: 명령어 가져오기
- PC(프로그램 카운터): 다음에 실행할 명령어의 주소를 담는다.
- 명령어 메모리: PC가 가리키는 주소의 32비트 명령어를 읽는다.
- PC + 4 덧셈기: 다음 명령어 주소를 계산한다. RISC-V 기본 명령어는 4바이트이므로 PC에 4를 더한다.
이 단계가 끝나면 32비트 명령어와 PC+4가 준비된다.
ID: 해석하고 레지스터 읽기
- 제어/디코드: opcode와 funct 필드를 보고, 이 명령어가 어떤 연산이며 이후 단계에서 각 유닛을 어떻게 제어할지 결정한다.
- 레지스터 파일 (읽기): 2편에서 본 대로
rs1,rs2가 고정 위치에 있으므로, 디코딩과 동시에 두 소스 레지스터 값을 읽는다. - 즉시값 생성기: 명령어에 흩어진 즉시값 비트를 재조립하고 부호 확장한다.
레지스터 파일은 한쪽에서 읽고 다른 쪽에서 쓸 수 있다. 읽기는 ID에서, 쓰기는 WB에서 일어난다.
EX: 계산하기
- ALU(산술논리연산 유닛): 실제 계산을 담당한다. 덧셈, 뺄셈, 논리 연산, 비교가 여기서 이뤄진다.
- 분기 타겟 덧셈기: 분기 명령어의 목적지 주소(
PC + 오프셋)를 계산한다.
ALU의 입력은 명령어 종류에 따라 달라진다. R 타입은 두 레지스터 값, I 타입은 레지스터 값과 즉시값을 받는다. 이 선택은 ID에서 만든 제어 신호가 결정한다. 로드/스토어의 주소 계산(베이스 + 오프셋)도 ALU가 덧셈으로 처리한다.
MEM: 메모리 접근
- 데이터 메모리: load는 계산된 주소에서 값을 읽고, store는 그 주소에 값을 쓴다.
메모리에 접근하는 명령어는 load와 store뿐이다(1편의 load/store 아키텍처). 나머지 명령어는 이 단계에서 아무것도 하지 않고 통과한다. ALU 연산 명령어의 결과는 이 단계를 그대로 지나 WB로 간다.
WB: 결과 쓰기
- 결과 선택 MUX: 레지스터에 쓸 값을 고른다. ALU 연산 결과인가(R/I 타입), 메모리에서 읽은 값인가(load). 명령어에 따라 둘 중 하나를 선택한다.
- 레지스터 파일 (쓰기): 선택된 값을 목적지 레지스터
rd에 쓴다.
store와 분기는 레지스터에 쓸 결과가 없으므로 이 단계에서 아무것도 쓰지 않는다.
파이프라인 레지스터: 단계를 잇는 경계
데이터패스 그림에서 스테이지 사이의 회색 막대가 파이프라인 레지스터다. 파이프라인을 성립시키는 핵심 요소다.
각 단계는 한 클럭 동안 일하고, 그 결과를 다음 단계로 넘겨야 한다. 파이프라인 레지스터가 이 넘김을 담당한다. 클럭이 올라가는 순간, 각 단계의 출력이 다음 파이프라인 레지스터에 저장된다. 다음 클럭에 그 값은 다음 단계의 입력이 된다.
IF ─[IF/ID]─ ID ─[ID/EX]─ EX ─[EX/MEM]─ MEM ─[MEM/WB]─ WB
레지스터 레지스터 레지스터 레지스터
파이프라인 레지스터가 없다면 한 단계의 결과가 정리되기 전에 다음 단계로 흘러 들어가 뒤섞인다. 파이프라인 레지스터는 각 단계를 시간적으로 격리해, 서로 다른 클럭에 속한 다섯 명령어가 서로 간섭 없이 동시에 진행되게 한다.
각 파이프라인 레지스터는 다음 단계가 필요로 하는 모든 것을 함께 넘긴다. 예를 들어 ID/EX 레지스터는 읽은 레지스터 값, 즉시값, 그리고 EX 이후 단계를 제어할 신호까지 담아 EX로 넘긴다. 목적지 레지스터 번호 rd도 명령어를 따라 WB까지 함께 흘러간다.
한 명령어의 여정
add x5, x6, x7이 파이프라인을 지나는 과정을 정리하면 이렇다.
IF : PC가 가리키는 곳에서 명령어 0x...를 읽음. PC+4 계산.
ID : R 타입 add로 디코드. x6, x7 값을 읽음. 제어 신호 생성.
EX : ALU가 x6 + x7 계산.
MEM : (메모리 접근 없음) 결과를 그대로 통과.
WB : 계산 결과를 x5에 씀.
로드 lw x5, 8(x6)이라면 EX에서 x6 + 8 주소를 계산하고, MEM에서 그 주소의 값을 읽고, WB에서 x5에 쓴다. 스토어 sw x7, 8(x6)이라면 EX에서 주소를 계산하고, MEM에서 그 주소에 x7을 쓰고, WB에서는 아무것도 하지 않는다.
명령어 종류가 달라도 다섯 단계를 모두 거친다는 점이 중요하다. 어떤 명령어는 특정 단계에서 아무 일도 하지 않지만, 단계 자체는 건너뛰지 않고 통과한다. 이렇게 해야 모든 명령어가 같은 리듬으로 파이프라인을 흐를 수 있다.
아직 남은 문제
지금까지는 명령어들이 서로 독립적이라고 가정했다. 그러나 실제 프로그램에서는 명령어들이 서로 얽힌다.
add x5, x6, x7 ← x5를 계산 (WB는 5번째 클럭)
sub x8, x5, x9 ← x5를 사용 (ID는 3번째 클럭)
두 번째 명령어가 x5를 읽으려는 시점(ID, 3번째 클럭)에, 첫 명령어는 아직 x5를 쓰지 못했다(WB, 5번째 클럭). 파이프라인이 겹쳐 흐르기 때문에 생기는 충돌이다. 이런 문제를 해저드(hazard) 라 한다.
분기도 문제다. 분기 명령어가 어디로 갈지는 EX에서야 확정되는데, 그사이 파이프라인은 이미 다음 명령어들을 IF로 가져오기 시작했다. 잘못 가져왔다면 되돌려야 한다.
이 문제들을 하드웨어가 어떻게 감지하고 해결하는지, 그리고 분기를 어떻게 예측하는지가 다음 편의 주제다.
요약
| 항목 | 내용 |
|---|---|
| 5단계 | IF / ID / EX / MEM / WB |
| IF | PC, 명령어 메모리, PC+4 |
| ID | 디코드, 레지스터 읽기, 즉시값 생성 |
| EX | ALU 연산, 분기 타겟 계산 |
| MEM | 데이터 메모리 (load/store) |
| WB | 결과 선택 후 레지스터 쓰기 |
| 파이프라인 레지스터 | 단계를 격리해 5개 명령어 동시 진행 |
| 이득 | 레이턴시가 아니라 처리량 (매 클럭 1개 완료) |
RISC-V 파이프라인은 한 명령어의 처리를 다섯 단계로 나누고, 각 단계에 전담 하드웨어 유닛을 둔다. 단계 사이의 파이프라인 레지스터가 각 명령어를 시간적으로 격리해, 다섯 명령어가 충돌 없이 겹쳐 흐른다. 이 구조가 처리량을 끌어올린다. 그러나 명령어들이 서로 의존하거나 분기가 끼면 파이프라인은 매끄럽게 흐르지 못한다. 다음 편에서는 이 해저드를 하드웨어가 어떻게 처리하고, 분기를 어떻게 예측하는지 들어간다.
다음 글: RISC-V CPU #4 - 파이프라인 심화: 해저드 처리와 분기 예측 (예정)