1. hazard 고려하지 않은 회로도
내부에 파이프라인 레지스터만 그려두었다.
1. Fetch
branch, jump 는 고려하지 않았다. (사실 파이프라인 회로도에서는 점프를 고려하지 않은 회로도만 있다.)
PC + 4, 명령어 주소값을 IF/ID 파이프라인 레지스터로 전달한다.
(instruction memory는 실수로 뺐다... 맨 뒤에 추가될 예정이다. 원래 이 단계에서 그리는 것이 맞다.)
2. Decode
우선 R-Format만 고려하여 위와 같이 작성하였다.
위 부분에서 잘못된 부분이 아직은 많다.
1. RegWrite은 나중에 파이프라인으로 넘겼다가 나중에 받아와야 하고
2. write address 는 rd로 받는게 아니라 rd/rt 중에 골라야 하는데
3. 이것도 MEM/WB까지 넘겼다가 받아와야 한다.
이 내용은 Write Back 스테이지에서 고쳐보겠다.
3. Execute
immediate 값과 rd2 값 중 하나를 골라서 ALU로 넣을 수 있도록 하였다.
따라서 I-Format이 어느정도 고려되었다.
ALU의 연산결과는 바로 EX/MEM 레지스터로 보낸다.
아직 메모리에 대한 생각은 하지 않았다.
여기에 더해 ALU Control을 추가해주었다.
ALU Op 라는 2bit 신호는 main decoder 가 생성한 뒤, ID/EX 레지스터로 넘겨주었을 것이니 가져오고,
명령어의 funct 필드는 immediate 필드의 하위 6 bit를 보면 된다. (그림이 틀렸습니다ㅠㅠ)
4. Memory
메모리 스테이지에서는 위와 같이 회로를 추가했다.
- ALU의 두번째 src로 immediate 필드가 선택되었을 경우, rd2 가 저장할 데이터가 될 수 있으므로 EX/MEM 레지스터로 가져온 뒤, 저장할 데이터로 넣어준다.
- ALU의 연산 결과는 메모리의 주소가 될 수 있으니 주소로 넣어준다.
- ALU의 연산 결과가 산술/논리 연산의 결과일 수 있으니 따로 MEM/WB 레지스터에 빼준다.
분기 여부를 결정하기 위해 branch 신호와, ALU에서 연산한 결과에서 zero 신호를 and 연산하여 PC src 신호를 생성한다.
직전 그림에서 변경된 점들
- ID 단계에서 opcode를 control unit 에 들어가도록 추가
- funct 필드 6bit로 수정
- PC + 4 값을 ID/EX 필드에 넣고, EX 스테이지에서 sign ext 된 값을 << 2 한 값과 PC + 4 를 더해서 분기할 주소값을 계산했다. 계산한 값은 (왜인지 모르겠으나... ) EX/MEM 레지스터에 담아서 다음 스테이지로 넘긴다.
- EX/MEM 스테이지에서 연산된 분기 주소를 IF 스테이지에 있는 mux로 들여보낸다.
이로써 분기 명령어에 대한 처리도 완료되었다.
5. Write Back
MEM/WB 레지스터에서 MemToReg 신호를 받은 뒤, ALU 연산값과 메모리에서 읽은 값 중 하나를 골라 wd 포트로 들여보낸다.
마지막으로 wa 를 rt, rd 중에 결정한 뒤, 그 값을 wa에 넣어주면 된다.
이때 이 값을 결정하는 mux는 EX 스테이지에 존재하는데, 이는 나중에 data hazard를 해결할 forwarding 유닛이 ex 스테이지에 들어가기 때문이다.
(그리고 바보같이 빼먹었던 IF단계의 instruction memory도 추가했다..)
2. Data Hazard 해결
1. forwarding unit
포워딩을 하는 과정은 크게 2가지로 나뉜다.
1. 포워딩이 필요한지 확인한다.
2. 포워딩이 필요하다면 포워딩한다.
만약 포워딩을 한다면, ALU의 2개 src에 대해서 각각
현재 명령어에서 읽어온 rs/rt 를 소스로 사용한다 / 직전 명령어의 rt/rd 결과를 소스로 사용한다 / 직직전 명령어의 rt/rd 결과를 소스로 사용한다.
3가지 중 하나를 선택해야한다.
우선 이렇게 선택하는 mux를 각각의 필드 앞에 추가해준다.
mux에 들어오는 컨트롤 신호는 Forwarding Unit이 생성할 것이며, 이는 그 다음에 추가할 것이다.
다음과 같이 그렸다.
빨간색 선은 직전 명령어의 ALU 연산 결과값
형광펜으로 그읏 하얀색 선은 직직전 명령어의 ALU 연산 결과값
하얀색 선은 현재 명령어에서 읽어온 rt, rt 필드의 레지스터 값이다.
이제 이 3가지 값 중에 어떤 것을 선택할 것인지 결정하는 forwarding unit을 그려보자.
포워딩을 해야 하는 상황은 아래와 같다.
[EX 해저드 = 직전 명령어의 실행결과에 의존하는 상황]
0. 직전 RegWrtie 필드가 활성화 되어있었다.
1. 직전 명령어의 rt/rd 레지스터 번호가 0이 아니다.
2. 현재 명령어의 rs/rt 필드 레지스터번호와 직전 명령어의 rt/rd 레지스터 번호가 같다.
만약 현재 명령어의 rs 와 같다면 Forwarding A, rt와 같다면 Forwarding B 상황이다.
[MEM 해저드 = 직직전 명령어의 실행결과에 의존하는 상황]
0. 직직전 RegWrtie 필드가 활성화 되어있었다.
1. 직직전 명령어의 rt/rd 레지스터 번호가 0이 아니다.
2. EX해저드가 아니다. (EX 해저드와 MEM 해저드가 모두 해당되면, EX해저드로 봐야한다.)
3. 현재 명령어의 rs/rt 필드 레지스터번호와 직직전 명령어의 rt/rd 레지스터 번호가 같다.
만약 현재 명령어의 rs 와 같다면 Forwarding A, rt와 같다면 Forwarding B 상황이다.
따라서 포워딩 유닛이 포워드 상황을 평가하기 위해 필요한 값은
1. 직전 RegWrite, 직직전 RegWrtie
2. 직전 rt/rd 레지스터 번호, 직직전 rt/rd 레지스터 번호 (rt, rd 중 reg_dst에 따라 선택된 번호만 있으면 된다.)
3. 현재 rs 레지스터 번호
4. 현재 rt 레지스터 번호
이다.
이를 그림으로 나타내면 위와 같다.
이제 포워딩 유닛에서 각 mux로 결과만 보내주면 된다.
2. Load-Use 케이스
Load-Use 케이스는 어쩔 수 없이 stall이 한번 발생한다.
그나마 stall 을 한번만 하도록 하려면, 메모리에서 읽어온 값을 WB 스테이지를 거치기 전에 바로 ALU로 넘기는 것을 거치는 과정도 필요하다.
우선 메모리에서 읽어온 값을 WB 스테이지를 거치기 전에 바로 ALU로 넘기도록 회로를 개선해보자.
이 과정은 간단하다.
기존에는 형광색으로 표시한 라인처럶, MEM/WB 레지스터에서 나온 ALU 연산 결과만을 src로 넘기고 있었다.
이 대신, MemToReg 컨트롤 신호를 거친 결과물을 src 후보로 넘기면 된다.
그러면 그림과 같이 수정할 수 있다.
형광색 라인이 바뀐 것을 확인하자.
정말 마지막으로, store 명령어를 실행할 때, 저장할 레지스터의 데이터 경로를 바꿔야 한다.
기존에 무조건 rd2 에서 데이터를 가져오던 기존 회로를
이렇게 3가지 forwarding 선택된 결과값에서 가져오도록 고쳤다.
빨간색 형광펜 라인이 추가되고, 기존에 어떤 라인이 삭제되었는지 확인해보자.
이것으로 직직전 명령어가 load 명령어였을 때 발생하는 data hazard는 해결하였다.
다음으로 직전 명령어가 load 였을 때 발생하는 data hazard를 해결해보자.
이건 stall을 할 수 밖에 없다.
stall을 하기 위해서 hazard detect unit 이라는 별도의 컨트롤을 추가하자.
이 컨트롤이 하는 일은 크게 2가지다.
1. load-use 케이스가 발생했을 때 stall
2. control hazard 에서 분기 예측에 실패했을 경우 flush
2번은 나중에 보기로 하고, 먼저 stall 부터 구현하자.
먼저 load-use 케이스가 발생했다는 것은 어떻게 알 수 있을까?
직전 명령어가 load 명령어였다는 것은
1. 직전에 MemRead가 활성화 되었었다.
2. 직전에 rt 필드 레지스터 번호와 현재 rs / rt 필드의 레지스터 번호가 같다.
이렇게 2가지를 체크하면 된다.
이 정보는 현재 명령어의 ID 단계에서 모두 알 수 있기 때문에 hazard detect unit은 ID 단계에 배치한다.
위 그림과 같이 배치된다.
ID 스테이지 상단에 배치한 hazard detect unit 과 그 유닛으로 들어오는 input 라인들을 따라가보면 이해하기 쉽다.
이제 load-use 케이스를 감지했을 때, hazard detect unit이 하는 일을 보면
1. 현재 가져온 명령어는 잘 놔둔 상태에서
2. 직전 명령어가 MEM 스테이지에서 데이터를 읽어오는 과정이 완료 될 때까지 1 사이클을 멈추고
3. 현재 가져온 명령어를 이어서 실행시킨다.
이렇게 3가지 단계를 거쳐야 한다.
따라서 현재 명령어의 정보를 보관하고 있는 IF/ID 파이프라인 레지스터의 값을 보존하기 위해 이 레지스터의 Write 신호를 끈다.
이것만 하면 현재 명령어 이후의 명령어를 fetch 한뒤 레지스터에 저장하려고 할 때, 저장은 못하고 다음 명령어가 버려질 수 있다.
따라서 현재 명령어 직후 명령어를 fetch 하지 않도록 PC 레지스터에 대한 Write 신호도 끈다.
마지막으로 현재 명령어는 이미 Decode까지 된 상태인데, Decode 된 이후에 실행이 되면 안되는 상황이다.
따라서 명령어의 실행을 막기 위해, 모든 컨트롤 신호를 0으로 만든다.
이렇게 3가지 기능을 수행하면 1cycle을 stall 할 수 있다.
위와 같이 회로를 그리면 된다.
3. Control Hazard
현재 보고있는 MIPS 회로는 분기 예측을 해서 실패할 경우 기존에 fetch 한 명령어를 flush 하는 과정을 취한다.
분기 예측을 빠르게 하기 위해 제일 먼저 개선할 것은, 분기 여부를 Decode 단계에서 판단하는 것이다.
이를 위해서, 기존에 분기 여부를 판별했던 회로 (branch 신호와 zero 신호, PC src 신호 생성) 를 Decode 단계로 옮긴다.
우선 분기할 주소를 계산하는 로직을 EX 단계에서 Deocde 단계로 옮겼다.
ID 단계에서 형광펜으로 칠한 경로를 보면 된다.
그렇다면 분기여부를 ALU 도움없이 어떻게 파악할 수 있을까?
간단히 rd1, rd2 가 같은지 bit by bit로 비교하는 comparator를 두면 된다.
강의록에는 branch 신호라든가 이런 게 회로에 빠져있어서 조금 헷갈리지만, 나름대로 추가하면 아래와 같이 나온다.
이제 분기 여부를 확인했을 때 예측에 실패하여 분기하는 로직이 추가되었다.
마지막으로 분기예측에 실패하여 분기하게 될 경우, 기존에 Fetch 해온 명령어를 flush 하는 회로를 추가한다.
이건 간단하게 IF.flush 신호를 IF/ID 레지스터에 넣어주면 된다.
(이건 내 생각이지만 PCsrc 컨트롤 신호를 이용해서 넣어주면 될 것 같다.)
그러면 이렇게 그려진다.
(PCsrc 에서 점선으로 뺀 이유는 확실하지 않아서..ㅎㅎ 강의록에는 IF.flush 신호가 그냥 밖에서 들어오는 형태로 그려져있었고, PCsrc 신호가 없었다.)
'CS > 컴퓨터 구조' 카테고리의 다른 글
[컴퓨터 구조] 28. Virtual Memory (3) - TLB 와 캐시 사이의 동작에 대한 고찰 (질문 정리) (0) | 2024.06.07 |
---|---|
[컴퓨터 구조] 27. Virtual Memory (2) - TLB (0) | 2024.06.07 |
[컴퓨터 구조] 26. Virtual Memory (1) - 개요 (0) | 2024.06.06 |
[컴퓨터 구조] 25. Cache (3) - 성능 개선 (0) | 2024.06.06 |
[컴퓨터 구조] 24. Cache (2) - Direct-Mapped Cache (0) | 2024.06.04 |