우리가 지금까지 본 Integer ADD(정수 더하기)는 1사이클 만에 실행(EX)이 끝났지만, 실수(Floating Point) 연산은 훨씬 복잡해서 시간이 오래 걸립니다. 이게 파이프라인에 들어오면 그동안 배웠던 규칙들이 깨지기 시작합니다. 교수님이 좋아하시는 "예외 상황"이 바로 여기서 터집니다.
기존 5단계(IF-ID-EX-MEM-WB)에서 EX 단계가 엄청나게 길어지거나 복잡해진다고 상상해보세요.
Integer Unit: EX가 1사이클 (빠름)
FP Add/Sub: EX가 4사이클
FP Multiply: EX가 7사이클
FP Divide: EX가 24사이클 (매우 느림)
이걸 처리하기 위해 현대 CPU는 EX 단계에 여러 개의 전용 연산기(Functional Units)를 병렬로 둡니다.
그림처럼 ID 단계에서 명령어를 해석한 뒤, 정수면 정수 유닛으로, 곱셈이면 곱셈 유닛으로 보냅니다.
명령어마다 실행 시간이 다르기 때문에(Multicycle), 끝나는 순서가 뒤죽박죽이 됩니다. 이를 Out-of-Order Completion이라고 하며, 이로 인해 새로운 해저드가 생깁니다.
순서가 뒤바뀌면서 "과거의 망령"이 현재를 덮어쓰는 문제입니다.
상황:
DIV.D F0, F2, F4 (F0에 저장, 24사이클 걸림)
... (다른 명령어들) ...
ADD.D F0, F6, F8 (F0에 저장, 4사이클 걸림)
문제: 3번 ADD는 빠르니까 금방 끝나서 F0에 값을 씁니다. 그런데 아주 나중에 1번 DIV가 끝나서 F0에 옛날 값을 덮어써 버립니다.
결과: F0에는 최신 값(ADD 결과)이 남아야 하는데, 엉뚱한 값(DIV 결과)이 남게 됩니다.
해결책: ID 단계에서 "이미 F0를 타겟으로 하는 명령어가 실행 중인가?"를 검사해서, 있다면 뒤의 명령어를 멈춰야(Stall) 합니다. (혹은 나중에 배울 Register Renaming으로 해결)
상황: 덧셈기(Adder)나 곱셈기(Multiplier)는 내부적으로 파이프라인이 되어 있어 매 사이클 입력을 받을 수 있지만, 나눗셈기(Divider)는 너무 복잡해서 파이프라인이 안 되어 있는 경우가 많습니다.
문제: 앞의 나눗셈이 안 끝났는데 뒤에서 또 나눗셈 명령어가 들어오면 충돌합니다.
해결: ID 단계에서 나눗셈기가 바쁜지(Busy) 체크하고 Stall을 겁니다.
우리가 배웠던 Forwarding(포워딩)도 만능이 아닙니다.
MUL.D(곱셈) 결과를 바로 다음 명령어에서 쓰려면, 1사이클이 아니라 6~7사이클을 기다려야(Stall) 합니다. 컴파일러가 이 사이에 다른 명령어들을 채워 넣느라 고생하게 됩니다.
이게 가장 골치 아픈 문제입니다.
상황: 명령어 A(오래 걸림)와 명령어 B(빨리 끝남)가 동시에 실행 중입니다. B가 먼저 끝나서 문제가 없었는데, 나중에 A에서 에러(Overflow 등)가 터졌습니다.
문제: 이미 B 이후의 명령어들이 실행되어 레지스터 상태를 바꿔놨을 수도 있습니다. "에러 난 시점의 정확한 상태"로 되돌리기가 매우 어렵습니다.
결론: 이를 Imprecise Exception(부정확한 예외)라고 하며, 이를 해결하기 위해 아까 잠깐 언급한 ROB(Reorder Buffer)나 히스토리 버퍼 같은 복잡한 하드웨어가 필요해집니다.
요약: 교수님 대비용 멘트
교수님이 "Floating Point 파이프라인을 쓰면 뭐가 제일 힘들어지나?"라고 물으시면 이렇게 답하세요.
"실행 시간이 서로 다른(Multicycle) 명령어들이 섞이면서 명령어 완료 순서가 뒤바뀝니다(Out-of-Order Completion). 이 때문에 기존 정수 파이프라인에선 없던 WAW 해저드가 발생하고, 나눗셈기 같은 자원에서 구조적 해저드가 생깁니다. 무엇보다 예외가 발생했을 때 정확한 시점으로 복구하기 힘든 Imprecise Exception 문제가 가장 큰 챌린지입니다."