4 분 소요

컴퓨터 구조도 30점 만점에 26.5점을 받았다. 아마 만점자도 있겠고 고득점자도 있겠지만, 평균이 21점에 중앙값이 22.75인 것을 보면 썩 괜찮게 본 듯?

이번 주 수요일(10/30)은 텀프로젝트에 대한 TA 세션이라서 수업 대신 과제 설명이 있었다. 새로 팀원이 된 동기 분과 열심히 과제를 해보자

Oct 28 (Mon)

멀티 사이클 프로세서의 개론

싱글 사이클 프로세서의 가장 큰 문제점인, longest propagation delay로 인한 사이클 길이 증가를 해결하기 위해 등장한 것이 멀티 사이클 프로세서다. 기존의 사이클을 여러 substage로 나누어서 각 명령어마다 다른 사이클 수를 사용하도록 하였다.

다만 이 과정에서 사이클이 늘어나기에, 중간에 임시 저장을 위한 여러 레지스터들이 추가되었다. 또한 하나의 유닛들로 여러 작업을 수행하기 위해 MUX의 개수가 증가했다. 따라서 이번엔 추가된 레지스터들과 MUX의 값 및 ALU 계산에 주목하며 하나하나 살펴보자.

기본적으로 ‘명령어 인출 -> 레지스터 인출 -> 실행 -> 메모리 접근 -> 메모리에 쓰기’의 다섯 substages로 구성되며, 분기(branch) 명령어는 세 사이클을, R-type(산술 논리) 명령어 및 저장(Store) 명령어는 네 사이클을, 값을 읽어오는 Load 명령어는 가장 긴 다섯 사이클을 쓰게 된다.

또한 싱글사이클에서 Adder가 하던 PC 계산을 ALU에 모두 몰아넣어서 ALU 하나로 모든 계산이 가능하다. 다만 상술했듯 기존의 registers 외에도 다른 레지스터들이 추가되었다.

명령어 레지스터(fetch된 명령어 임시 저장), 레지스터 A(rs1 임시 저장), B(rs2 임시 저장), ALUOut(ALU 계산값을 임시 저장), MDR(Memory Data Register: load해온 값 임시 저장)가 추가되었다. 또한 MUX도 0, 1에 이어 2까지의 값도 갖게 된다. 체크하자.

Step 1: Instruction Fetch

여기선 PC(프로그램 카운터)의 값에 해당하는 명령어를 인출(fetch)하고 그 다음 PC를 계산해두는 절차가 진행된다. PC값은 먼저 MUX=0을 통과해 메모리를 거쳐 명령어 레지스터까지 전달된다. 또한 별개의 datapath를 따라 각각 MUX=0의 기존 PC값과 MUX=1의 상수 4가 전달되어 ALU에서 PC+4로 더해진다.

더해진 PC+4의 값은 ALUOut에는 저장되지 않고(이번 스텝에서 바로 PC로 보내지기에 굳이 임시 저장할 필요가 없음) PC를 담당하는 MUX=0으로 전해져서 바로 다음 PC값으로 갱신까지 마무리된다. 이 갱신된 PC값은 후에 분기 명령어에서 덮어씌워져 사라지기도 한다. 별일이 없으면 갱신된 그대로 다음 명령어 인출에 사용된다.

Step 2: Instruction Decode and Register Fetch

이제 명령어 레지스터에 저장된 rs1과 rs2를 실제 registers를 거쳐 각각 register A, register B에 저장하여 해독(decoding)을 진행한다. 여기서 중요한 지점은 분기 명령어를 대비해 분기 주소를 미리 계산해둔다는 것이다.

명령어 레지스터에서 IMM32값을 가져와 MUX=2를 통과시키고, 기존에 fetch된 PC값을 다시 MUX=0으로 통과시켜 ALU에서 더해준다. 이 값은 ALUOut 레지스터에 임시 저장된다.

이렇게 하는 이유는 뒤에서 다시 말하겠지만, 명령어 해석 과정이 오래 걸려 아직 마무리 되지 않았기에, 이렇게 분기 주소를 미리 계산하면 branch 명령어의 사이클 횟수를 줄일 수 있기 때문이다. 혹여나 decoding된 명령어가 branch일 것을 대비하기 위함이라고 생각하면 편하다.

R type: Step 3-4

먼저 4사이클을 사용하는 산술논리연산(R-Type)부터 살펴보자.

Step 3 for R type: R-Type Execution

명령어 해독을 해보니 branch가 아니라 R 타입의 명령어였다. 아뿔싸! 우리는 이미 분기를 위한 PC 주소를 ALUOut에 저장하고 있다. 이를 어찌한단 말인가?

상관없다. 그냥 새로 계산된 A op B의 값으로 덮어씌워버리자. 깔끔하게 Step 2에서 계산된 값을 버린다. A 레지스터의 MUX=1과 B 레지스터의 MUX=0을 통과하여 op를 ALU에서 계산하고 이를 ALUOut에 다시 저장한다.

Step 4 for R type: R-Type Completion(rd에 저장)

다음 사이클에선 ALUOut의 값을 MUX=0을 통과하여 레지스터에 저장(RegWrite=1)한다. 이때 rd는 명령어 레지스터에서 가져온다. 산술논리연산이 4스텝 만에 종료되었다!

Branch: Step 3

명령어 해독을 해보니 운이 좋게도 분기 명령어였다! 다행이다. 우리는 Step 2에서 이미 분기 주소를 계산해두었으므로, 실제로 분기를 할지 말지만 결정하여 행동하면 된다!

Step 3 for Branch: Branch Completion(분기해 말어)

두 조건이 같은지(A==B)를 확인하기 위해 레지스터 A의 MUX=1과 레지스터 B의 MUX=0을 통해 ALU에서 zero flag에 대한 계산을 한다. 만약 Zero가 0(A!=B)이 나왔다면 아쉽게 된 거다. 분기 못하는 거지 뭐.

하지만 Zero==1(A==B)이었다면 두 번째 스텝에서 계산해둔 ALUOut의 값을 MUX=1을 통과시켜 PC로 보낸다. 우리가 첫 번째 스텝에서 계산해두었던 PC+4의 값은 봉쇄되었다. 이제 새로운 분기값으로 PC값이 옮겨졌으니, 새로운 곳에서 새로운 명령어로 새 출발을 해보자!

Load: Step 3-5

가장 긴 Load 명령어는 무려 다섯 사이클이나 사용한다! 찬찬히 살펴보자

Step 3 for Load (& Store): Memory Execution

Load 명령어를 위해선 값을 읽어올 메모리의 주소가 필요하다. 따라서 ALUOut에 저장되어있는 분기 주소는 더 이상 필요 없다. 이 역시 덮어씌워 없애버리자. 레지스터 A에 저장된 배열(혹은 그 외)의 첫 주소를 MUX=1로 불러오고, IMM32값을 MUX=2를 통해 가져와 ALU에서 더해주자.

이제 계산된 값을 ALUOut에 덮어씌워준다. 다음 스텝에선 이 ALUOut을 본격적으로 메모리에 쏴줄 것이다.

Step 4 for Load: Load Memory Access

MemRead=1, MemWrite=0을 통해 ALUOut에 저장되었던 메모리의 주소를 찾아가 값을 읽어온다. 그러나 datapath를 이미 거쳤기에 불러온 값의 저장은 다음 스텝으로 미뤄야할 듯 하다. 따라서 MDR(Memory Data Register)에 값을 임시 저장하고 턴을 마무리하자.

Step 5 for Load: Load Completion(레지스터에 값 저장)

MDR에서 잠시 대기하던 load한 데이터를 MUX=1을 통해 Register에 저장해준다. 당연히 여기서 RegWrite=1이다. 수고했다. 가장 긴 Load 명령어를 수행했다. 근데 무언가 찝찝하다. 우리가 아직 안 살펴본 명령어 하나가 남아있지 않았나..?

Store: Step 3-4

울지마 저장 명령어야, 널 잊지 않았어.

항상 Load에 밀려 존재감을 잊히고 마는 Store다. 자기 이름이 불릴 때까지 정말 많이 기다렸을테니 다뤄주자. 근데 Step 3는 Load와 동일해서 Step 4만 보면 된다.

Step 4 for Store: Store Memory Access

ALUOut에는 저장할 메모리의 주소가 계산되어있다. 이제 MemRead=0, MemWrite=1로 하여 메모리에 값을 저장해보자. 이때 저장할 값은 rs2로, 레지스터 B에 저장되어 있었다. 어서 메모리로 쏴주자. 이제 저장 명령어까지 마무리 되었다!

멀티 사이클에서 주목할 점

일단 2번째 스텝에서 분기 주소를 계산한다는 점이 중요하다. 사실 이 값은 분기 시에만 쓰인다. 그런데 왜 2번째 스텝에서 벌써 분기 주소를 계산해둔단 말인가?

명령어 해독은 상당히 긴 시간을 필요로 한다. 따라서 아직 명령어가 뭔지도 모르는 상황이기에, 해당 명령어가 branch가 아닐 지라도 일단은 계산을 해두는 것이다.

혹자는 “그럼 branch인지 해독 후, 그제서야 분기 주소를 계산하면 되지 않냐?”라고 하는데, 그러면 branch명령어의 온전한 해독을 위해 두 사이클을 사용하면서 총 4 사이클을 쓰게 된다. 비효율적이고 성능이 나빠진다. 따라서 분기 주소는 2번째 스텝에서 진행한다.

또한 제어 신호(control signals)들이 더이상 명령어에만 좌우되지 않는다. (이게 뭔 뜻인지 좀 더 찾아봐야할듯..)

멀티 사이클의 장단점

장점은 역시나 사이클의 길이가 감소한다는 것이다. 당연하다. 기존 싱글 사이클의 사이클 하나를 substage를 통해 여러 개로 잘라두었는데 당연히 짧아졌겠지. 이 덕분에 비교적 단순한 분기 연산이나, 산술 논리 연산 등의 명령어를 더 짧은 시간에 실행할 수 있게 되었다.

또한 하나의 unit(register나 ALU 등)을 여러 명령어의 경우에서 계속 돌려쓰므로, 더 적은 유닛 구현이 필요해 경제적이다.

그러나 단점으로는 Instruction, A, B, ALUOut, MDR 등 추가 레지스터가 필요하다는 점, 그리고 무엇보다 각 명령어의 타이밍이 다 달라 이를 조정하는 데에 힘이 든다는 것이다. 또한 각 substage의 path를 잘못 구현하면, 전체적인 실행(성능)이 악화될 수 있다.

출처: 컴퓨터구조(COSE222) ㄱㅇㄱ 교수님