CPU 파이프라이닝 시각화와 브랜치 예측
이 글은 제 브랜치 예측 시리즈의 일부입니다.
- CPU 파이프라이닝 시각화
- 브랜치 예측이 실제 데이터를 필요로 하는 이유
- LLDB를 활용한 브랜치 예측 평가 (준비 중)
시작하며
CPU 파이프라이닝에 대해 배운 내용을 공유하고 싶습니다.
Dan Luu의 브랜치 예측 글 덕분에 개념적으로는 어느 정도 알고 있었습니다. 그런데 Rodrigo Copetti의 PlayStation MIPS 글을 읽으면서 브랜치 딜레이 슬롯이 어떻게 진화해 브랜치 예측이 되었는지를 보니, 더 깊이 파고들 동기가 생겼거든요. 파이프라이닝에는 생각보다 많은 미묘한 디테일들이 숨어 있더라고요.
더 자세한 내용은 『Computer Organization and Design』 4장을 추천합니다.
이 글은 여러분이 CPU 파이프라이닝의 기본 개념인 "세탁소" 혹은 "조립 라인" 모델은 알고 있지만, 하위 수준의 세부 사항에서는 조금 헷갈리는 상태라고 가정합니다. 또한 5단계 MIPS 파이프라인에 대해 대략적인 이해가 있으면 도움이 됩니다. (좋은 요약이 있습니다) 이 글에서 다루는 모델 CPU는 32비트 MIPS입니다.
파이프라인 시각화하기
먼저 파이프라이닝이 없는 기본 CPU 모델(단일 사이클 CPU)을 시각화해봅시다.
한 번에 CPU의 한 부분만 활성화되고 나머지는 유휴 상태인 게 병목입니다. 파이프라이닝이 도입되면서 명령어가 각 단계를 순차적으로 통과하게 되었습니다. 한 명령어가 완전히 끝날 때까지 기다리지 않고요. 마치 여러 사람이 일하는 조립 라인처럼 보입니다.
그런데 이렇게 하면 몇 가지 미묘한 문제가 생깁니다.
명령어 디코딩
명령어 디코딩은 다른 단계들이 사용할 필드들을 제공해서 전체 파이프라인을 조율합니다.
간단한 예를 들어봅시다. 명령어가 add $t1, $s2, $s3이라고 하면:
IF 단계는 명령어를 가져와 ID 단계 레지스터에 넣습니다. 이제 ID 레지스터에는 나머지 모든 단계에서 사용할 필드들이 있습니다.
pc: 0x014b4820
->
Field op rs rt rd shamt funct
Binary 000000 01010 01011 01001 00000 100000
# add $s2 $s3 $t1
- EX는 ALU 연산을 위해 op, rs, rt를 사용합니다
- MEM (lw, sw 명령어용)은 rt를 사용합니다
- WB는 rd 레지스터에 결과를 씁니다
그런데 파이프라이닝에서는 문제가 생깁니다. 예를 들어 3사이클에서 add는 EX로 이동하고 sub는 ID로 이동합니다. 하지만 WB 단계는 add 명령어의 rd 필드를 알아야 하는데, 그 필드가 있던 ID 레지스터를 sub가 덮어써버리죠.
해결책은 각 파이프라인 단계 사이에 레지스터를 두는 겁니다. 이 레지스터들이 ID 단계 필드들(그리고 다른 단계들의 필드들도) 전달합니다.
이제 add가 WB에 도달했을 때, 자신의 rd 필드를 가지고 있어서 올바른 레지스터에 결과를 쓸 수 있습니다. EX나 MEM의 실제 값도 MEM/WB 레지스터에 저장됩니다.
해저드 감지
ID 단계의 필드 메타데이터는 각 단계에서 레지스터 쓰기 같은 기본 연산을 가능하게 합니다. 그런데 이 필드들이 데이터 해저드를 해결하는 데도 핵심입니다.
이 예를 봅시다:
add $t1, $s2, $s3
sub $t2, $t1, $s4
sub가 add의 출력에 의존하고 있습니다. 이를 "데이터 해저드"라고 합니다. 구조적으로는 CPU가 계속 진행할 수 있지만 결과는 틀릴 거예요. ID 단계의 명령어 입력이 다운스트림 명령어의 출력 레지스터와 일치하는지 확인할 방법이 필요합니다.
각 단계 레지스터에 필드 메타데이터가 전파되므로, 필요한 데이터는 모두 있습니다. 부족한 건 이를 계산할 유닛뿐입니다:
ID/EXE.rs == EX/MEM.rd || ID/EXE.rt == EX/MEM.rd ||
ID/MEM.rs == MEM/WB.rd || ID/MEM.rt == MEM/WB.rd
이게 해저드 감지 유닛(Hazard Detection Unit, HDU)입니다. 이 유닛은 해저드를 감지하고 그것이 해결될 때까지 진행을 멈춥니다.
이 로직은 add나 and처럼 레지스터에 쓰는 R-타입 명령어에는 잘 작동하지만, sw처럼 레지스터에 쓰지 않는 명령어에는 적용 안 됩니다. 완벽한 구현을 위해서는 lw와 sw가 관련된 데이터 해저드 같은 몇 가지 조건을 더 확인해야 합니다.
실선은 HDU의 입력을 나타내며, 해저드를 감지합니다. 점선은 제어 신호로, PC와 IF를 멈추고 ID/EX 레지스터에 nop 명령어를 씁니다.
nop은 각 단계를 거쳐 결국 해저드 비교 로직이 거짓이 됩니다. 그러면 PC와 IF 단계의 정지가 풀리고, sub 명령어가 계속 진행할 수 있도록 하는 신호가 제거됩니다.
왜 정체를 "버블"이라고도 부를까요? 한 번 나타나면 파이프라인을 따라 흐르다가 끝에서 터지거든요. 물속에서 공기를 내보내는 다이버처럼, 버블이 수면으로 올라갑니다. 비유를 더 확장하면, HDU가 다이버처럼 상황에 따라 하나 이상의 버블을 방출할 수 있습니다. 위의 예시에서는 2개의 버블을 방출했죠.
포워딩
데이터 해저드를 해결하는 또 다른 방법이 있습니다. 정체시키는 대신 중간 결과를 다음 명령어로 "포워딩"하는 겁니다.