IT/컴퓨터구조와 운영체제

[Pipeline CPU] in RV32I CPU microarchitecture

kykyky 2023. 12. 23. 00:08

이 글에 포함된 모든 사진 자료들은

<원리부터 설계까지 쉽고 명확한 컴퓨터구조> (서태원 지음)

에서 가져왔습니다.    

 

 

Pipeline CPU란

 

pipeline CPU란, single-cycle CPU 구조를 여러 stage로 나누어, 각 부분을 동시에 활용되게끔 하는 것이다.

(아래 사진 참고)

 

 

single-cycle CPU에서는, 하나의 instruction이 완료되고 나서야 그 다음 instruction을 수행한 데 비해,

pipeline CPU의 경우, 매 clock cycle의 각 stage에서는 다른 instruction이 실행되고 있다.

(아래 사진 참고)

 

 

 

전형적인 것은 5-stage pipeline CPU이며, 각 stage는 다음과 같다:

  • IF(Instruction Fetch): 명령어를 읽는 단계
  • ID(Instruction Decoding): 명령어를 해석하고 source operand를 준비하는 단계
  • EXE(Execution): 명령어를 실행하는 단계
  • MEM(Memory Access): 메모리에 접근하는 단계
  • WB(WriteBack): 레지스터 파일을 업데이트하는 단계

 

 

이같이 단계를 나누려면, 나누고자 하는 곳에 flip flop을 삽입하여야 한다. 또한 이를 통해 데이터뿐만 아니라 control 신호도 함께 다음 단계로 전달해야 한다.

(아래 사진 참고)

 

 

하지만 이러한 pipeline CPU 구조는 Hazard를 발생시키므로, 이에 대한 해결책이 필요하다.

 

 


Data Hazard란

 

Data hazard는 instruction 실행 과정에서의 data depencence 때문에 일어난다.

(data dependence: 앞의 명령어가 변경하는 레지스터를 뒤의 명령어가 사용하는 경우)

여기서 "앞의 명령어"가 무엇인지에 따라 경우가 나뉜다.

 

앞의 명령어가 "데이터 처리 명령어"인 경우

데이터 처리 명령어의 경우, 문제점과 해결책

 

   문제점

예를 들어 위와 같이 명령어가 순서대로 있을 때,

첫 명령어의 연산 결과로 인해 s0가 변경되고, 다음 명령어는 이 s0를 가지고 연산한다.

그런데 첫 명령어에서 s0가 업데이트되는 시점은 WB의 끝, 즉 clock #5의 끝인데,

다음 명령어가 s0를 사용하는 것은 EXE부터, 즉 clock #4의 시작부터이다. 즉 이것이 시간 상 먼저 일어나버리는데, 이 시점에는 아직 첫 명령어에 의한 s0 업데이트 이전이므로,

의도와 다른 해석이 이루어지고 만다.

 

   해결책

그런데 실제로 첫 명령어에서 새로운 s0 값이 결정되는 것은, WB의 끝까지 기다릴 필요 없이, 첫 명령어의 EXE 과정에서 ALU를 거친 직후이다. 따라서 이 값을 미리 다음 명령어의 EXE에서 활용되기 이전에 전달해주면 해결되며, 이것이 Data forwarding이다.

 

이러한 Data Forwarding이 필요한 모든 경우는 다음과 같다.

 

 

   이러한 data forwarding 여부를 결정하기 위해선 data hazard의 발생 여부를 먼저 판단해야 한다. 이는 data dependency 관계에 있는 두 명령어 중 앞선 명령어의 destination register와 후속 명령어의 source register를 비교하여, 동일한 경우 hazard가 발생한다고 판단하여 필요한 control 신호를 만든다.

 

 

앞의 명령어가 "load 명령어"인 경우

 

 

   이 경우에는 위의 방법만으로는 해결되지 않는다. 위 경우에서는 앞선 명령어의 결과값이 ALU의 출력 이후 바로 가져올 수 있었지만, 앞의 명령어가 load 명령어인 경우에는, 메모리에서 읽은 데이터가 유효한 것이 MEM의 끝부터이기에, 이때는 후속 명령어의 EXE 시작 부분에 Data forwarding을 해줄 수 없다.

따라서 불가피하게 후속 명령어들을 잠시 EXE 이전 단계에 머무르게 하여, 앞선 명령어를 기다려주게끔 해야 한다. 이것이 Stall이다.

(아래 사진 참고)

 

그림의 (a-1)은 stall 없이 data forwarding만 해주는 것이 불가능함을 설명하고 있다.

따라서 (a-2)와 같이 stall 후 data forwarding해 주어야 한다.

 

 load 명령어의 destination register와 후속 명령어의 source register가 동일한 경우 stall 신호는 1이 된다.

 

 


Control hazard란

 

control hazard는 분기 명령어로 인해 실행 흐름에 변화가 생길 때 발생한다.

위 예시에서, beq 명령어로 인해 실제로 분기될지 여부 (즉 taken 여부)는, EXE 단계에서의 ALU가 't0 - t1'을 연산함으로써 결정된다.

문제는, taken 여부가 결정된 시점에는 이미 이후 명령어들이 실행되고 있었으므로,

"taken이 참이라서 다음 명령어들은 건너뛰어졌어야 하는 경우" 문제가 발생한다.

 

 

위 경우, 분기 명령어 직후 실행되지 않았어야 하는 명령어들을 flush함으로써 해결할 수 있다.

(아래 사진 참고: 두번째, 세번째 명령어는 flush됨) 

 

IF와 ID 단계에 추가된 mux는 btaken_exe 신호에 따라 정상 control 신호 혹은 nop control 신호 중 선택하며, 후자의 경우 nop은 파이프라인 레지스터를 통해 다음 단계로 흘러가 해당 명령어가 flush됨으로써 건너뛰어지는 것이다.

 

 

이때 btaken_exe 신호는 어떻게 만들어지는 것인가?

 

  조건 분기 명령어의 경우, ALU 연산 결과에 따라 0 또는 1로 결정되도록, ALU 이후에 로직을 만든다.

 

   그런데 무조건 분기 명령어의 경우에는 언제나 분기를 수행하여 control hazard를 발생시키므로, 일찍이 이것의 ID 단계에서 알아챌 수 있다.

따라서 이러한 무조건 분기 명령어가 ID 단계에 있을 때 jal_id와 같은 control 신호를 만들면, 이는 파이프라인 레지스터를 통해 EXE 단계로 넘어가 jal_exe신호를 만들고, 이는 btaken_exe과 동일하게 작동하여 nop을 발생시킨다.

(아래 사진 참고)

 


 

아래는 Data hazard와 Control hazard를 해결하는 (간소화된) 로직이 포함된 CPU 구조이다.