ISA에서 하드웨어로: 파이프라인
ISA는 계약서일 뿐 회로가 아니다. 그런데 계약서를 어떻게 쓰느냐에 따라 그것을 지키는 회로가 쉬워지기도, 어려워지기도 한다. 이 장에서는 RV32I를 직접 단일 사이클 데이터패스와 5단 파이프라인으로 구현해 보면서, 해저드·포워딩·분기 예측·정밀한 예외 같은 구현의 고민을 만난다. 그리고 고정된 레지스터 칸, 부호 비트 위치, 플래그 없음, 지연 슬롯 없음 같은 RISC-V의 결정이 그 고민을 어떻게 덜어 주는지 확인한다.
- PC·명령어 메모리·레지스터 파일·즉시값 생성기·ALU·데이터 메모리·WB 선택기로 이루어진 단일 사이클 데이터패스를 그림으로 설명한다.
- 명령 분류(R·I·적재·저장·분기·jal)마다 어떤 제어 신호가 켜지는지 안다.
- IF·ID·EX·MEM·WB 다섯 단계와 파이프라인 레지스터의 역할을 이해한다.
- RAW 데이터 해저드를 포워딩으로 없애고, 적재-사용 해저드에서 왜 한 사이클 멈춰야 하는지 설명한다.
- EX 단계에서 판정되는 분기가 왜 손해를 만드는지, 정적·동적 분기 예측이 그것을 어떻게 줄이는지 시뮬레이터로 측정한다.
- RISC-V의 인코딩·설계 결정이 해독, 비순차 실행, 정밀한 예외를 어떻게 쉽게 만드는지 정리한다.
명령 하나를 한 사이클에: 단일 사이클 데이터패스
가장 단순한 구현부터 시작하자. 클럭이 한 번 똑딱일 때마다 명령 하나를 처음부터 끝까지 처리하는 CPU다. 이런 CPU에서 데이터가 흐르는 길을 데이터패스(datapath)라 하고, 어느 길을 열고 닫을지 정하는 부분을 제어 유닛(control unit)이라 한다. RV32I의 거의 모든 명령은 다음 일곱 부품만 있으면 처리할 수 있다.
- PC: 지금 실행할 명령의 주소를 담은 레지스터.
- 명령어 메모리: PC가 가리키는 32비트 명령을 꺼낸다.
- 레지스터 파일: x0–x31. 읽기 포트 두 개(rs1, rs2)와 쓰기 포트 한 개(rd).
- 즉시값 생성기: I·S·B·U·J 형식에 흩어진 즉시값 비트를 모아 32비트로 부호 확장한다(4장).
- ALU: 더하기·빼기·논리·시프트·비교.
- 데이터 메모리: 적재·저장이 접근하는 메모리.
- 선택기(mux): ALU 두 번째 입력(rs2냐 즉시값이냐), 레지스터에 쓸 값(ALU·메모리·PC+4), 다음 PC(PC+4냐 분기 목적지냐)를 고른다.
아래 데이터패스에서 명령 분류를 바꿔 보자. 그 명령이 실제로 쓰는 선만 파란 점선으로 흐르고, 쓰지 않는 부품은 흐려진다. 선 옆의 작은 글자는 제어 유닛이 내보내는 제어 신호(control signal)의 값이다.
단일 사이클 RV32I 데이터패스
제어 유닛은 opcode(그리고 funct3·funct7)만 보고 아래 표의 신호를 만든다. RISC-V는 형식이 여섯 가지뿐이고 opcode 칸이 언제나 [6:0]에 있어서, 이 표가 작고 규칙적이다.
| 분류 (예) | RegWrite | ALUSrc | ImmSel | ALUOp | MemRead | MemWrite | WBSel | 다음 PC |
|---|---|---|---|---|---|---|---|---|
R형 add | 1 | rs2 | — | funct3·funct7 | 0 | 0 | ALU | PC+4 |
I형 산술 addi | 1 | imm | I | funct3 | 0 | 0 | ALU | PC+4 |
적재 lw | 1 | imm | I | 더하기 | 1 | 0 | 메모리 | PC+4 |
저장 sw | 0 | imm | S | 더하기 | 0 | 1 | — | PC+4 |
분기 beq | 0 | rs2 | B | 비교 | 0 | 0 | — | 성립 시 PC+imm |
점프 jal | 1 | — | J | — | 0 | 0 | PC+4 | PC+imm |
간접 점프 jalr | 1 | imm | I | 더하기 | 0 | 0 | PC+4 | (rs1+imm) & ~1 |
상위 즉시값 lui | 1 | imm | U | B 통과 | 0 | 0 | ALU | PC+4 |
lw(800 ps)에 클럭을 맞춰야 한다. add는 600 ps, beq는 500 ps면 충분한데도 매번 800 ps를 기다린다. 점선 상자가 그 낭비다. 이 낭비를 줄이려는 것이 파이프라인의 출발점이다.한 사이클 안에 명령도 읽고 데이터도 읽어야 하므로 포트가 두 개 필요하다. 실제 칩에서는 명령어 캐시와 데이터 캐시를 따로 두는 방식(하버드식 L1)으로 이 요구를 맞춘다. 규격은 이런 구조를 전혀 요구하지 않는다. 그래서 명령어 쓰기가 명령어 가져오기에 보이게 하려면 fence.i(16장)로 명시적으로 동기화해야 한다.
5단 파이프라인: IF · ID · EX · MEM · WB
빨래를 생각해 보자. 세탁기가 돌아가는 동안 건조기를 놀리지 않으려면, 첫 빨래가 건조기로 넘어가자마자 다음 빨래를 세탁기에 넣으면 된다. CPU도 똑같다. 명령 처리를 다섯 단계로 자르고, 단계 사이에 값을 잠시 붙잡아 두는 파이프라인 레지스터(pipeline register)를 넣으면, 다섯 명령이 동시에 서로 다른 단계에 머물 수 있다. 명령 하나가 걸리는 시간(지연(latency))은 줄지 않지만, 단위 시간에 끝나는 명령 수(처리량(throughput))가 크게 는다.
lw 세 개를 실행하는 시간. 단일 사이클은 2400 ps, 5단 파이프라인은 1400 ps가 걸린다. 파이프라인에서는 레지스터 읽기·쓰기(100 ps)처럼 짧은 단계도 가장 긴 단계(200 ps)에 맞춰 한 클럭을 다 쓰므로, 명령 하나의 지연은 오히려 1000 ps로 늘었다. 그래도 명령이 많아지면 처리량은 거의 4배가 된다. 금색 세로선은 흘러가는 시간이다.| 단계 | 하는 일 | RISC-V 인코딩이 돕는 점 |
|---|---|---|
| IF 가져오기 | PC로 명령어 메모리를 읽고 PC+4를 계산한다. | C 확장이 없으면 모든 명령이 4바이트이고 4바이트 정렬이라 한 번에 한 명령씩 깔끔하게 읽힌다. |
| ID 해독 | opcode를 해독하고, 레지스터 파일에서 rs1·rs2를 읽고, 즉시값을 만든다. | rs1·rs2 칸 위치가 고정이라 해독과 동시에 레지스터를 읽는다. 부호 비트가 언제나 inst[31]이다. |
| EX 실행 | ALU 연산, 적재·저장 주소 계산, 분기 조건 비교와 목적지 계산. | 비교와 분기가 한 명령이라 조건 코드 레지스터가 필요 없다. |
| MEM 메모리 | 적재는 데이터 메모리를 읽고, 저장은 쓴다. 다른 명령은 통과한다. | 메모리에 접근하는 명령은 적재·저장뿐이다(load-store 구조). 주소 지정 방식이 “레지스터+12비트 오프셋” 하나다. |
| WB 쓰기 | 결과를 레지스터 파일의 rd에 쓴다. | 명령 하나가 쓰는 정수 레지스터는 많아야 하나(rd)다. |
값만이 아니다. ID/EX에는 rs1·rs2 값, 즉시값과 함께 그 명령이 뒤 단계에서 쓸 제어 신호(MemRead, MemWrite, RegWrite, WBSel)와 목적 레지스터 번호 rd도 실린다. 제어 신호가 명령과 함께 “따라 내려가기” 때문에, 각 단계는 자기 앞에 있는 막대만 보고 일할 수 있다. 다음 절의 해저드 검출도 바로 이 rd 번호를 비교한다.
데이터 해저드와 포워딩
파이프라인은 명령이 서로 독립일 때 가장 잘 돈다. 하지만 실제 프로그램은 앞 명령의 결과를 바로 다음 명령이 쓰는 일이 흔하다. 다음 코드를 보자.
add는 5번째 사이클의 WB에서야 x1을 레지스터 파일에 쓰는데, sub는 3번째 사이클의 ID에서 x1을 읽는다. 쓰기 전에 읽으려는 이 상황을 RAW 해저드(read-after-write hazard)라 한다. 가장 단순한 해결은 sub를 ID에 붙잡아 두는 것(멈춤(stall))이지만, 두 사이클이나 손해다.
그런데 잘 보면 x1의 값은 3번째 사이클 EX가 끝날 때 이미 계산되어 EX/MEM 레지스터에 있다. 레지스터 파일을 거치지 말고 그 값을 다음 명령의 ALU 입력으로 바로 돌려보내면 된다. 이것이 포워딩(forwarding, bypassing)이다.
sub는 4번째 사이클 EX에서 EX/MEM 레지스터의 x1을, and는 5번째 사이클 EX에서 MEM/WB 레지스터의 x1을 받는다. or는 5번째 사이클 ID에서 레지스터 파일을 읽는데, 같은 사이클 앞 절반에 WB가 먼저 쓰도록 설계하면 포워딩 없이도 새 값을 본다. 결국 이 네 명령은 한 사이클도 멈추지 않는다.포워딩 유닛은 EX 단계에 들어온 명령의 rs1·rs2 번호를 앞선 두 명령의 rd 번호와 비교하는 작은 비교기 묶음이다. 조건을 그대로 적으면 다음과 같다(rs2도 똑같다).
| 우선 | 조건 | ALU 입력 A로 보낼 값 |
|---|---|---|
| 1 | EX/MEM.RegWrite 이고 EX/MEM.rd ≠ 0 이고 EX/MEM.rd = ID/EX.rs1 | EX/MEM의 ALU 결과 (바로 앞 명령) |
| 2 | MEM/WB.RegWrite 이고 MEM/WB.rd ≠ 0 이고 MEM/WB.rd = ID/EX.rs1 | MEM/WB의 쓰기 값 (두 칸 앞 명령) |
| 3 | 그 밖 | ID에서 레지스터 파일로 읽은 값 |
1번이 2번보다 앞서는 이유는, 두 명령이 모두 같은 레지스터를 썼다면 더 최근 명령의 값이 맞기 때문이다. 그리고 rd가 0인지 따로 검사하는 이유는 x0 때문이다.
x0에 쓰는 값은 버려지고 x0를 읽으면 언제나 0이다(3장). 따라서 addi x0, x0, 0(nop)이나 jal x0, …(j) 같은 명령은 아무리 많아도 진짜 의존성을 만들지 않는다. 포워딩·해저드 검출 비교기에 “rd ≠ 0” 조건 하나만 붙이면 이 모든 경우가 걸러진다. RISC-V는 이렇게 쓰지 않는 결과를 버리는 곳으로 x0를 일관되게 써서, 비교 결과만 내는 별도 명령이나 “결과 없음” 표시 비트를 따로 두지 않았다.
적재-사용 해저드: 피할 수 없는 한 사이클
포워딩으로도 해결할 수 없는 경우가 있다. lw의 값은 데이터 메모리를 읽는 MEM 단계가 끝나야 나온다. 그런데 바로 다음 명령이 그 값을 EX에서 쓰려면, 같은 사이클의 시작에 값이 있어야 한다. 아직 존재하지 않는 값을 시간을 거슬러 보낼 수는 없으므로, 해저드 검출 유닛이 다음 명령을 ID에 한 사이클 붙잡고 EX에 아무 일도 하지 않는 거품(bubble)을 넣는다.
lw의 값은 4번째 사이클 MEM이 끝나야 생기므로, 4번째 사이클 EX로 보낼 수 없다(빨간 점선). add를 한 사이클 붙잡아 5번째 사이클 EX에서 MEM/WB 레지스터로부터 받는다(보라 화살표). 뒤따르는 sub도 함께 한 칸 밀린다.포워딩이 없을 때와 있을 때 멈추는 사이클 수를 의존 거리별로 정리하면 다음과 같다. 의존 거리 1은 “바로 다음 명령이 쓴다”는 뜻이다.
| 생산자 → 소비자 | 거리 1 | 거리 2 | 거리 3 이상 |
|---|---|---|---|
| ALU 명령 → 사용, 포워딩 없음 | 2 | 1 | 0 |
| ALU 명령 → 사용, 포워딩 있음 | 0 | 0 | 0 |
| 적재 → 사용, 포워딩 없음 | 2 | 1 | 0 |
| 적재 → 사용, 포워딩 있음 | 1 | 0 | 0 |
남은 한 사이클은 하드웨어 대신 컴파일러가 숨길 수 있다. 적재와 그 값을 쓰는 명령 사이에 서로 관계없는 명령을 끼워 넣는 명령어 스케줄링(instruction scheduling)이다.
초기 MIPS(R2000)는 이 한 사이클을 아예 ISA에 적어 넣었다. “적재 바로 다음 명령은 아직 새 값을 볼 수 없다”는 적재 지연 슬롯이다. 하드웨어는 해저드 검출 회로가 필요 없어졌지만, 그 규칙은 특정 파이프라인 구조를 계약서에 박제해 버렸고 MIPS II에서 사라졌다. RISC-V는 이런 구현 세부를 ISA에 넣지 않는다. 멈출지 말지는 구현이 알아서 정하고, 소프트웨어는 언제나 순서대로 실행된 것과 같은 결과를 본다.
제어 해저드와 분기 예측
분기 명령은 또 다른 문제를 만든다. 다음에 가져올 명령이 무엇인지 분기 조건을 계산해 봐야 아는데, 그 계산(두 레지스터 비교)은 EX 단계에서 끝난다. 그동안 IF는 놀 수 없으니 무언가를 가져와야 한다. 이 책의 파이프라인은 분기를 EX에서 판정하고, 그동안 일단 다음 주소(PC+4)를 계속 가져온다. 이것이 “불성립 예측”이다. 예측이 틀리면 그사이 가져온 두 명령을 폐기(flush)한다.
beq가 3번째 사이클 EX에서 “성립”으로 판정되면, 그사이 가져온 PC+4·PC+8의 명령을 폐기하고 4번째 사이클부터 목적지 L을 가져온다. 성립하는 분기마다 2사이클을 잃는다. 판정을 ID로 앞당기면 손해가 1사이클로 줄지만, 비교기를 ID에 두면 그쪽으로도 포워딩이 필요해 회로가 복잡해진다.정적 예측: 뒤로 가면 성립, 앞으로 가면 불성립
루프의 맨 끝 분기는 대부분 성립해서 뒤(낮은 주소)로 간다. 반대로 if 문의 예외 처리 같은 앞쪽 분기는 대개 성립하지 않는다. 그래서 하드웨어 없이도 꽤 잘 맞는 규칙이 BTFN(backward taken, forward not taken)이다. RISC-V의 B형 즉시값은 부호 비트가 언제나 inst[31]에 있으므로, 분기의 방향(앞이냐 뒤냐)을 해독 전에 비트 하나만 보고 알 수 있다. 규격은 소프트웨어에게 이 규칙을 전제로 코드를 배치하라고 권한다.
“Software should be optimized such that the sequential code path is the most common path, with less-frequently taken code paths placed out of line. Software should also assume that backward branches will be predicted taken and forward branches as not taken, at least the first time they are encountered.”
Volume I, RV32I · Conditional Branches — 같은 절은 무조건 점프에는 조건이 항상 참인 분기 대신 jal x0(j)을 쓰라고 한다. 그래야 조건 분기 예측 표를 더럽히지 않는다.
동적 예측: 2비트 포화 카운터
정적 규칙은 분기의 실제 행동을 배우지 못한다. 분기 이력 표(BHT, branch history table)는 분기 주소마다 작은 카운터를 두고, 실제 결과를 볼 때마다 갱신한다. 가장 유명한 것이 2비트 포화 카운터다. 한 번 틀렸다고 곧바로 예측을 뒤집지 않기 때문에, 루프 끝에서 한 번 빠져나가는 일 때문에 다음 루프 첫 반복을 또 틀리지 않는다.
분기 예측기 실험실
5단 파이프라인 시뮬레이터
이제 지금까지의 규칙을 모두 합쳐 보자. 아래 시뮬레이터는 프로그램을 먼저 이 책의 RV32I 에뮬레이터(Riscv.Machine)로 실제 실행해 진짜 실행 순서(어느 분기가 성립했는지)를 얻은 다음, 그 순서를 5단 파이프라인에 흘려보낸다. 따라서 표에 나타나는 분기 결과는 언제나 실제 실행과 같다. 모델의 가정은 이 장의 본문과 같다.
- 분기(
beq등)와jalr는 EX에서 판정한다.jal의 목적지는 ID에서 계산한다. - 레지스터 파일은 앞 절반에 쓰고 뒤 절반에 읽는다. 포워딩 경로는 EX/MEM → EX와 MEM/WB → EX다.
- 분기 목적지 버퍼(BTB)가 없다고 가정한다. 그래서 “성립”으로 예측해도 목적지를 아는 ID 단계에서야 방향을 바꾸므로 1사이클을 잃는다. 예측이 틀리면 EX에서 바로잡으며 2사이클을 잃는다.
- 프로그램은
ebreak나 종료 시스템 호출(li a7, 10+ecall)로 끝낸다. 실행 명령은 최대 120개까지만 그린다.
RV32I 5단 파이프라인
RISC-V의 설계가 파이프라인을 돕는 방법
지금까지 본 해저드와 해법은 어느 ISA를 구현해도 만나는 것이다. 하지만 ISA가 어떻게 생겼느냐에 따라 같은 일을 하는 회로의 크기와 속도가 달라진다. RISC-V 규격은 비고(commentary)에서 이런 구현상의 이유를 여러 번 밝힌다.
① 레지스터 칸은 언제나 같은 자리
R·I·S·B 형식 모두 rs1은 [19:15], rs2는 [24:20], rd는(있다면) [11:7]이다. 그래서 ID 단계는 opcode가 무엇인지 알기 전에 이 두 칸으로 레지스터 파일을 읽기 시작할 수 있다. 쓰지 않는 칸을 읽었다면 결과를 버리면 그만이다. 해독과 레지스터 읽기가 병렬로 진행되는 것이다.
“Decoding register specifiers is usually on the critical paths in implementations, and so the instruction format was chosen to keep all register specifiers at the same position in all formats at the expense of having to move immediate bits across formats …”
Volume I, RV32I · Base Instruction Formats — 같은 절은 “the sign bit for all immediates is always in bit 31 of the instruction to speed sign-extension circuitry”라고도 적는다. 즉시값 비트가 뒤섞인 이유(4장)가 바로 이 대가다.
② 나머지 결정들
| RISC-V의 결정 | 파이프라인에서 얻는 것 | 다른 ISA의 예 |
|---|---|---|
| 조건 코드(플래그) 없음 — 비교와 분기를 한 명령으로 | 산술 명령이 숨은 출력(플래그)을 만들지 않아 해저드 검출·포워딩·이름 바꾸기 대상이 범용 레지스터뿐이다. 분기가 프런트엔드에서 일찍 보인다. | x86·Arm·SPARC·PowerPC는 플래그 레지스터를 쓴다. |
| 지연 슬롯 없음 | 분기 판정이 어느 단계에서 끝나든, 몇 개를 동시에 가져오든 ISA가 상관하지 않는다. 깊은 파이프라인과 슈퍼스칼라에 짐이 없다. | MIPS·SPARC는 분기 지연 슬롯을 ISA에 넣었다. |
| 부호 비트는 언제나 inst[31] | 즉시값 부호 확장을 해독과 병렬로 시작하고, 분기 방향(앞/뒤)을 비트 하나로 안다. | — |
| 고정 32비트 명령, 4바이트 정렬 (C 확장 없을 때) | 가져오기와 명령 경계 찾기가 쉽다. C 확장이 있어도 길이는 하위 2비트만 보고 안다(13장). | x86은 1–15바이트 가변 길이라 경계를 찾는 일 자체가 어렵다. |
| 메모리 접근은 적재·저장뿐, 주소 방식 하나 | MEM 단계를 지나는 명령이 적고, 정수 적재·저장 명령 하나는 메모리에 한 번만 접근한다(벡터 확장 등은 예외). | x86은 산술 명령도 메모리 피연산자를 가진다. |
| 정수 명령은 소스 ≤ 2, 목적지 ≤ 1 | 레지스터 파일 포트(읽기 2, 쓰기 1)와 이름 바꾸기 회로를 작게 유지한다. | Arm의 일부 명령은 쓰는 레지스터가 둘 이상이다(예: 사후 증가 주소 방식). |
“Control transfer instructions in RV32I do not have architecturally visible delay slots.”
Volume I, RV32I · Control Transfer Instructions — 같은 장의 비고는 비교와 분기를 합친 이유로 “a combined compare-and-branch instruction fits into a regular pipeline, avoids additional condition code state”를 든다.
더 나아가: 슈퍼스칼라와 비순차 실행
5단 순차 파이프라인의 이상적인 CPI는 1이다. 그보다 빨라지려면 한 사이클에 여러 명령을 내보내는 슈퍼스칼라(superscalar), 그리고 준비된 명령부터 먼저 실행하는 비순차 실행(out-of-order execution)이 필요하다. 비순차 코어의 핵심 부품은 레지스터 이름 바꾸기(register renaming)다. 같은 아키텍처 레지스터(예: a0)에 쓰는 명령마다 새 물리 레지스터를 배정해, 진짜 의존성(RAW)만 남기고 가짜 의존성(WAR·WAW)을 없앤다.
jnz가 두 줄 위 sub가 남긴 FLAGS를 읽는 숨은 의존성을 추적해야 하고, add·sub가 FLAGS를 쓸 때마다 새 물리 레지스터(p70, p71)를 배정해야 한다. 오른쪽 RISC-V는 bnez가 a2를 읽는다는 사실이 명령어에 적혀 있다.그 밖에도 RISC-V 구현자들이 즐겨 쓰는 기법이 있다.
- 매크로 연산 융합(macro-op fusion):
lui+addi,auipc+jalr,slli+add처럼 자주 붙어 다니는 두 명령을 해독 단계에서 내부 연산 하나로 합친다. ISA는 단순하게 두고 성능은 구현이 챙기는 방식이다. M 확장 규격이mulh+mul의 권장 순서를 정해 둔 것도 융합을 염두에 둔 것이다(9장). - 반환 주소 스택(RAS):
jalr의 rd·rs1이 x1/x5인지에 따라 “호출”인지 “반환”인지 알려 주는 힌트가 규격에 정해져 있어, 반환 주소를 예측하는 하드웨어 스택이 이를 이용한다(5장). - 가져오기 정렬: C 확장을 넣으면 IALIGN이 16비트가 되어 32비트 명령이 4바이트 경계나 캐시 줄 경계를 걸칠 수 있다. 그래서 C를 지원하는 코어는 가져오기 버퍼에서 명령 경계를 다시 맞춘다. 대신 명령 길이를 하위 2비트만 보고 알 수 있어 경계 찾기는 x86보다 훨씬 쉽다(13장).
버클리의 교육용·연구용 코어만 봐도 범위가 넓다. 5단 순차 파이프라인 Rocket, 비순차 슈퍼스칼라 BOOM이 같은 ISA를 구현한다. 그 위로 중국과학원의 XiangShan 같은 고성능 비순차 코어, 아래로는 2–3단짜리 마이크로컨트롤러 코어가 있다. 이 장의 5단 모델은 그 중간쯤, 교과서의 표준 모델이다.
파이프라인 속의 예외: 정밀한 트랩
파이프라인에는 여러 명령이 동시에 들어 있다. 그중 하나가 예외를 일으키면 어떻게 될까? 예를 들어 MEM 단계의 lw가 접근할 수 없는 주소를 읽으려 한다. 이때 그보다 먼저 들어온 명령은 끝까지 마쳐야 하고, 예외를 일으킨 lw와 그 뒤에 들어온 명령은 아무 흔적도 남기면 안 된다. 그래야 트랩 처리기가 보기에 “lw 직전까지 순서대로 실행된” 깨끗한 상태가 된다. 이것을 정밀한 예외(precise exception)라 한다.
lw가 접근 오류를 냈다. 앞선 add는 같은 사이클에 WB를 마치고, lw 자신과 뒤의 세 명령은 아무것도 쓰지 않은 채 사라진다. 6번째 사이클부터 mtvec이 가리키는 트랩 처리기를 가져온다. 빨간 점선이 “여기까지는 실행됐고, 여기부터는 실행되지 않았다”는 정밀한 경계다.5단 파이프라인에서 정밀성을 지키는 요령은 간단하다. 예외는 발견된 단계에서 바로 처리하지 않고 표시만 해서 파이프라인 레지스터에 실어 내려보낸다. 그리고 한 지점(보통 MEM 끝 또는 WB 직전, 커밋 지점(commit point))에서 프로그램 순서대로 확인한다. 그러면 ID에서 발견된 잘못된 명령 예외와 MEM에서 발견된 접근 오류가 동시에 생겨도, 더 앞선 명령의 예외가 먼저 처리된다. 레지스터와 메모리를 바꾸는 일이 모두 커밋 지점 뒤(MEM의 저장, WB의 쓰기)에 있다는 점이 이 방식을 가능하게 한다.
| 발견되는 단계 (전형적) | 예외 | mcause 예외 코드 |
|---|---|---|
| IF | 명령어 접근 오류 · 명령어 페이지 폴트 | 1 · 12 |
| ID | 잘못된 명령(illegal instruction) · ebreak · ecall(U/S/M) | 2 · 3 · 8/9/11 |
| EX | 분기·점프 목적지 정렬 오류(instruction address misaligned) · 적재/저장 주소 정렬 오류 | 0 · 4/6 |
| MEM | 적재 접근 오류 · 저장 접근 오류 · 적재/저장 페이지 폴트 | 5 · 7 · 13/15 |
표에서 눈여겨볼 점이 둘 있다. 첫째, 분기 목적지의 정렬 오류는 목적지 명령이 아니라 분기 명령에서 보고된다. 규격이 그렇게 정한 이유는 “IALIGN=32인 구현에서 정렬이 어긋날 수 있는 곳이 분기·점프뿐이라 하드웨어 설계가 단순해지고 디버깅도 쉽기 때문”이다. EX에서 목적지를 계산하는 순간 바로 알 수 있다. 둘째, 정렬되지 않은 적재·저장을 어떻게 처리할지는 실행 환경(EEI)이 정한다. 하드웨어가 두 번 나눠 접근해 처리할 수도, 보이지 않는 트랩으로 소프트웨어가 흉내 낼 수도, 예외를 낼 수도 있다. 자연 정렬된 접근만은 어떤 경우에도 정렬 예외를 내지 않는다.
“The EEI defines for each trap whether it is handled precisely, though the recommendation is to maintain preciseness where possible.”
Volume I, Introduction · Exceptions, Traps, and Interrupts — 트랩이 생겼을 때 mepc·mcause·mtval과 mstatus가 어떻게 바뀌는지는 18장에서 자세히 다룬다.
타이머 인터럽트처럼 바깥에서 오는 인터럽트는 특정 명령 때문이 아니므로, 구현은 파이프라인 안의 아무 명령 경계나 골라 그 앞까지 완료하고 나머지를 폐기하면 된다. 이때 mepc에는 “다시 실행할 첫 명령”의 주소가 기록된다. 파이프라인이 깊고 넓을수록 경계를 맞추는 일이 어려워지는데, 비순차 코어는 재정렬 버퍼(ROB)에서 명령을 순서대로 은퇴시키며 이 경계를 만든다.
핵심 정리
- 단일 사이클 데이터패스는 PC·명령어 메모리·레지스터 파일·즉시값 생성기·ALU·데이터 메모리·선택기로 RV32I를 구현한다. 가장 긴
lw의 경로가 클럭 주기를 정한다. - 5단 파이프라인(IF·ID·EX·MEM·WB)은 파이프라인 레지스터로 단계를 나눠, 명령 하나의 지연은 그대로 두고 처리량을 높인다.
- RAW 해저드는 EX/MEM·MEM/WB → EX 포워딩으로 대부분 없앤다. 비교에서 rd = x0인 경우는 제외한다.
- 적재-사용 해저드는 포워딩이 있어도 1사이클 멈춤이 필요하다. 컴파일러 스케줄링으로 숨길 수 있다.
- 분기를 EX에서 판정하면 예측이 틀릴 때 2사이클을 잃는다. BTFN 정적 예측과 2비트 BHT가 이 손해를 줄인다.
- 고정된 rs1·rs2 위치, inst[31]의 부호 비트, 플래그 없음, 지연 슬롯 없음, 적재·저장 구조가 해독·포워딩·이름 바꾸기를 단순하게 만든다.
- 정밀한 예외는 예외를 표시만 해서 내려보내고 커밋 지점에서 순서대로 처리하는 방식으로 지킨다.
확인 퀴즈
단일 사이클 RV32I CPU의 클럭 주기를 결정하는 명령은 보통 무엇인가?
포워딩이 있는 5단 파이프라인에서 lw x1, 0(x2) 바로 다음에 add x3, x1, x4가 오면 몇 사이클 멈추는가?
포워딩이 없고, 레지스터 파일이 같은 사이클에 앞 절반 쓰기·뒤 절반 읽기를 한다. add x1, … 바로 다음 sub x4, x1, x5는 몇 사이클 멈추는가?
분기를 EX에서 판정하고 “불성립”으로 예측하는 파이프라인에서, 실제로 성립한 분기 하나 때문에 폐기되는 명령은 몇 개인가?
포워딩·해저드 검출 조건에 “rd ≠ 0”을 넣는 이유는?
RISC-V에서 ID 단계가 opcode 해독이 끝나기 전에 레지스터 파일을 읽기 시작할 수 있는 이유는?
조건 코드(플래그)가 없다는 점이 비순차 코어에 주는 이점으로 가장 알맞은 것은?
파이프라인에서 “정밀한 예외”가 뜻하는 것은?
더 읽을거리
- The RISC-V Instruction Set Manual, Volume I — RV32I Base Integer Instruction Set (v20260120): 형식 설계, 지연 슬롯, 비교-분기, 정적 예측에 관한 비고
- Volume I — Introduction: Exceptions, Traps, and Interrupts (v20260120)
- Volume II — Machine-Level ISA: mcause 예외 코드와 예외 우선순위 (v20260120)
- David A. Patterson, John L. Hennessy, Computer Organization and Design RISC-V Edition: The Hardware/Software Interface, Morgan Kaufmann — 4장(프로세서)이 이 장의 단일 사이클·5단 모델의 원형이다.
- John L. Hennessy, David A. Patterson, Computer Architecture: A Quantitative Approach, 6th ed. (2017) — 분기 예측, 비순차 실행, 이름 바꾸기
- Andrew Waterman, “Design of the RISC-V Instruction Set Architecture”, UC Berkeley 박사 논문 (2016)
- James E. Smith, “A Study of Branch Prediction Strategies”, ISCA (1981) — 2비트 포화 카운터
- James E. Smith, Andrew R. Pleszkun, “Implementation of Precise Interrupts in Pipelined Processors”, ISCA (1985)