파이프라인 해저드와 CPU 명령어 의존성 처리
파이프라인 해저드의 구조적·데이터·제어 의존성과 Stall, Forwarding, 레지스터 리네이밍, 분기 예측 처리 방식을 정리한다.
2026-08-14 · 최초 발행 2025-12-30
명령어가 겹쳐 흐를 때 생기는 대기
파이프라인은 명령어 실행을 여러 단계로 나눠 겹쳐 처리한다. 각 단계가 막힘없이 진행되면 CPI(Cycles Per Instruction)는 1이 되고, 처리량은 파이프라인 단계 수배가 된다. 하지만 하드웨어 자원, 이전 명령어의 결과, 분기 결과에 대한 의존성이 있으면 다음 명령어는 예정된 클럭에 계속 실행되지 못한다.
이 현상이 파이프라인 해저드(Pipeline Hazard)다. 해저드가 발생하면 CPI는 1보다 커지고 처리량은 줄어든다.
자원·데이터·분기에서 갈리는 해저드
구조적 해저드는 여러 명령어가 같은 하드웨어 자원을 동시에 요구해 생긴다. 폰 노이만 구조에서 명령어와 데이터가 하나의 메모리와 포트를 공유하는 경우, 또는 ALU나 메모리 포트가 부족한 경우가 여기에 해당한다. 명령어 읽기와 데이터 읽기가 동시에 메모리에 접근하거나 여러 명령어가 하나의 ALU를 사용하려는 상황이 예다.
데이터 해저드는 선행 명령어가 만든 값을 후속 명령어가 필요로 하지만 그 결과가 아직 준비되지 않았을 때 발생한다.
ADD R1, R2, R3 ; R1 = R2 + R3
SUB R4, R1, R5 ; R4 = R1 - R5 (R1 값 필요)
제어 해저드는 JUMP, BRANCH 같은 분기 명령어가 명령어 흐름을 바꾸면서 발생한다. 분기 조건이 확정되기 전에는 순차 경로의 명령어가 파이프라인에 들어갈 수 있고, 분기 시 이 명령어들을 폐기해야 한다. 조건 분기뿐 아니라 무조건 분기와 CALL, RETURN도 같은 문제를 만든다.
BEQ R1, R2, Label ; R1 == R2이면 Label로 분기
ADD R3, R4, R5 ; 분기 안 하면 실행, 분기하면 폐기
SUB R6, R7, R8 ; 분기 안 하면 실행, 분기하면 폐기
Label:
MUL R9, R10, R11 ; 분기 시 실행
구조적 충돌을 줄이는 설계 선택
명령어 메모리와 데이터 메모리를 분리하고 독립적인 버스를 두는 하버드 아키텍처는 두 접근을 동시에 수행하게 해 구조적 해저드를 제거한다. 대신 하드웨어 복잡도와 비용이 증가한다.
ALU를 여러 개 두거나 다중 포트 메모리로 메모리 포트를 늘리는 방법도 있다. 병렬 연산과 동시 다중 접근이 가능해지고 대역폭이 증가하지만, 필요한 하드웨어 자원도 함께 늘어난다.
자원을 바로 확보할 수 없다면 Stall 또는 Bubble을 넣는다. Stall은 자원을 쓸 수 있을 때까지 파이프라인을 일시 중지하는 방식이며 NOP(No Operation) 삽입과 유사하다. Bubble은 빈 단계를 삽입해 의존성을 해소할 시간을 만든다.
결과값을 기다리지 않고 전달하는 방법
데이터 의존성을 가장 직접적으로 피하는 방법은 Stall이다. 의존성을 감지한 뒤 결과값이 준비될 때까지 파이프라인을 멈추고, 준비가 끝나면 실행을 재개한다. 안전하지만 CPI 증가와 처리량 감소를 감수해야 한다.
Bypass(Forwarding)는 결과값이 레지스터에 기록되기 전, 파이프라인 레지스터에서 다음 명령어의 실행 단계로 직접 전달한다. 이를 위해 Forwarding Unit, 추가 데이터 경로, Mux가 필요하다.
ADD R1, R2, R3 ; EX 단계에서 R1 값 생성
SUB R4, R1, R5 ; EX 단계에서 R1 값 Forwarding으로 즉시 사용
Delayed Load는 컴파일러가 의존성을 분석해 영향 없는 범위에서 명령어 순서를 바꾸고, 필요하면 NOP를 삽입하는 방식이다.
; 원본
LW R1, 0(R2) ; Load
ADD R3, R1, R4 ; R1 사용 (의존성)
; 최적화: 명령어 재정렬
LW R1, 0(R2) ; Load
SUB R5, R6, R7 ; 독립적인 명령어 (Delay Slot)
ADD R3, R1, R4 ; R1 사용
Register Renaming은 논리 레지스터를 다른 물리 레지스터로 바꿔 WAR와 WAW를 해소한다. 이 방식에서는 물리적 레지스터 수가 논리적 레지스터 수보다 많다.
; 원본 (WAW 발생)
ADD R1, R2, R3 ; R1에 쓰기
SUB R1, R4, R5 ; R1에 다시 쓰기 (충돌)
; Register Renaming
ADD R1_1, R2, R3 ; 물리 레지스터 R1_1 사용
SUB R1_2, R4, R5 ; 물리 레지스터 R1_2 사용
분기 결과를 다루는 방식
가장 단순한 대응은 분기 방향이 결정될 때까지 파이프라인을 정지하는 것이다. 분기 명령어를 감지하고 조건을 평가한 뒤 방향이 정해지면 실행을 재개한다. 안전하지만 그만큼 성능 손실이 생긴다.
분기 예측은 이 대기 구간을 줄이기 위한 방법이다. Predict Not Taken은 분기하지 않는다고 가정해 다음 명령어를 실행하고, 실제 분기 시 인출된 명령어를 Flush한다. Predict Taken은 분기 목표 주소에서 명령어를 인출하며, 실제로 분기하지 않으면 이를 취소한다.
정적 예측은 컴파일러 또는 하드웨어가 고정 전략을 사용하므로 단순하지만 정확도가 낮다. 동적 예측은 분기 이력을 바탕으로 Branch Prediction Table(BHT, BTB)을 사용하며 90-95%의 높은 정확도를 제공한다.
Delayed Branch는 분기 명령어 바로 다음 명령어를 항상 실행하는 Branch Delay Slot을 활용한다. 컴파일러는 영향 없는 명령어를 이 위치에 배치한다. MIPS, SPARC 등에서 사용되며 분기 손실을 줄일 수 있지만, 적절한 명령어를 찾기 어렵고 컴파일러 복잡도가 증가한다.
; 원본
BEQ R1, R2, Label
ADD R3, R4, R5 ; Delay Slot (항상 실행)
SUB R6, R7, R8
Label:
MUL R9, R10, R11
; 분기해도 ADD는 실행됨
RAW·WAR·WAW가 가리키는 의존성
RAW(Read After Write)는 선행 명령어가 결과를 쓰기 전에 후속 명령어가 해당 값을 읽으려는 True Dependency다. 가장 흔한 데이터 해저드이며 Stall과 Bypass(Forwarding)로 대응한다.
ADD R1, R2, R3 ; R1에 쓰기
SUB R4, R1, R5 ; R1 읽기 (RAW 해저드)
WAR(Write After Read)은 선행 명령어가 읽기 전에 후속 명령어가 값을 쓰는 Anti-Dependency다. Out-of-Order 실행에서 발생할 수 있고 Register Renaming으로 해결한다.
SUB R4, R1, R5 ; R1 읽기
ADD R1, R2, R3 ; R1에 쓰기 (WAR 해저드, Out-of-Order 시)
WAW(Write After Write)는 이전 결과가 기록되기 전에 후속 명령어가 같은 위치에 값을 쓰는 Output Dependency다. 이 역시 Out-of-Order 실행에서 발생하며 Register Renaming이 해결 방법이다.
ADD R1, R2, R3 ; R1에 쓰기
SUB R1, R4, R5 ; R1에 다시 쓰기 (WAW 해저드)
| 해저드 유형 | 해결 방법 |
|---|---|
| RAW | Stall, Bypass (Forwarding) |
| WAR | Register Renaming |
| WAW | Register Renaming |