4.1 Introduction
이 챕터는 프로세스의 구현방법에 대해서 다룬다. 아주 추상화/단순화된 관점에서부터 datapath를 만들고 간단한 버전의 MIPS를 구현하기에 이른다. 또한, 현실 세계에서 x86같은 복잡한 ISA 구현에 필수적인, 병렬적인(pipelined) 구현 방법 또한 다룬다.
우리는 일부 정수 인스트럭션이나, 부동 소수점 인스트럭션 등을 제외하고 아래의 3종류로 분류되는, 간단한 MIPS 인스트럭션들만을 구현할 것이다.
- 메모리 참조 인스터럭션: lw(load word), sw(store word)
- 산수연산 인스트럭션: add, sub, AND, OR, slt
- 브랜치 인스트럭션: beq(branch equal), j(jump)
큰 그림
모든 인스터럭션은 아래의 두 과정을 거친다.
- Program Counter(PC)를 메모리에 전달하여, 갖고 있는 코드를 꺼내온다.
- 인스트럭션의 필드를 이용하여, 해당하는 1개 또는 두개의 레지스터를 읽는다. lw 연산자는 하나의 레지스터만 읽지만, 대다수의 인스트럭션은 두개의 레지스터를 읽는다.
그 다음 과정은, 인스트럭션의 종류에 따라 달라진다. 다행히도 MIPS 인스트럭션의 설계는 단순하고(simplicity) 규칙적(regularity)이기에 대다수의 과정은 비슷하게 진행된다. jump를 제외한 모든 인스트럭션은 레지스터를 읽은 후에 ALU(arithmetic-logical unit)로 넘어간다.
- 메모리 참조 인스트럭션은 ALU로 주소를 계산한다. 이후, 메모리에 접근하여 데이터를 읽거나 쓴다. load 인스터럭션의 경우, 읽은 데이터를 레지스터에 다시 저장한다.
- 산수연산 인스트럭션은 연산 수행에 ALU를 사용한다. 수행된 결과는 다시 레지스터에 저장된다.
- 브랜치 인스트럭션은 ALU로 비교연산을 수행한 후, 다음 주소를 PC로 넘긴다. 특별한 주소로 이동하지 않는다면, 평소에 PC는 4가 더해져 다음 인스터럭션으로 이동해야 한다.

앞선 설명을 토대로 MIPS 구현체의 대략적인 모습이다. 눈여겨 볼점은 두 곳에서 나온 데이터가 한 곳으로 몰리는 곳이 있다는 것이다. 그림에서 실선의 한 중간에 화살표가 꽂혀있는 곳들이다. 이 곳에는 어떤 소스에서 나온 데이터를 이용할지 결정하는 multiplexor라는 장치가 필요하다.
또, 어느 장치들은 인스트럭션에 따라 다른 연산을 수행해야 하므로, 이것을 통제할 컨트롤 유닛(control unit)이 필요하다. 메모리가 이번에 데이터를 읽어야할지 써야할지, ALU가 어떤 연산을 수행해야 할지 등을 입력으로 전달해야 한다. 이 두가지 포인트를 적용하면 그림은 아래와 같이 복잡해진다.

4.2 Logic Design Conventions
Appendix C를 요약한 챕터이다.
4.3 Building a Datapath
Datapath는 우리의 프로세스에 필요한 구성요소들을 의미한다.

- Fetching Instruction
PC는 매 클락마다 업데이트된다. 클락 신호 외에는 어떤 컨트롤 신호도 필요없다. Memory는 쓰기 작업은 할 필요 없이 읽기 작업이면 충분하다. 그래서, combinational 하게 동작한다. 별도의 읽기 신호가 필요없다는 뜻이다.
- Executing R-format instruction
R-format 인스트럭션은 3개의 피연산자를 가진다. 두 개는 레지스터로부터 읽고, 하나는 레지스터에 쓴다. 데이터를 읽을때는 읽을 레지스터 번호 두 개를 입력으로, 읽은 데이터 두개를 출력으로 반환한다. 데이터를 쓸 때는, 쓸 데이터와 쓸 레지스터 번호가 입력으로 있으면 충분하다. 그래서 레지스터 파일은 4개의 입력과 2개의 출력을 가진다.
ALU는 두 개의 32비트 입력을 받아, 32비트 출력을 반환한다. 만약, 출력값이 0이라면 1비트짜리 추가 시그널을 Zero에 반환한다. 또, ALU는 다양한 연산을 수행하기에, 4비트짜리 컨트롤 라인도 입력으로 받는다. 추후에 다시 다룬다.

- Executing Load and Store instruction
Store 작업에서는 저장할 데이터를 레지스터로부터 읽어와야하고, Load 작업에서는 데이터를 레지스터에 써야한다. 그리고, 인스트럭션의 인자로 넘어온 16비트의 부호가 있는 offset-value로부터 메모리의 실제 주소를 연산해야한다. 이 과정에서 16비트를 32비트로 부호를 살려 확장하는 sign-extension을 수행할 회로와 ALU의 도움이 필요하다.


- beq instruction
이번에도 16비트 offset-value로부터 branch target address를 연산해야한다. 이 연산 과정은 챕터2에서 소개했지만, 여기서 다시 두 가지 중요한 디테일을 상기해보자.
- Offset과 더하는 값은 현재의 PC가 아니라 PC+4와 같은 다음에 실행될 인스트럭션의 주소값이다. 이미 우리 구현상 PC+4를 fetch instruction 단계에서 가져오는 것이 간편하기 때문에 그렇다.
- Offset은 byte가 아닌 word 단위로 들어온다. 즉, 4배 혹은 shift left 2 연산을 해주어야 우리가 찾는 byte address를 구할 수 있다.
만약 beq 안의 조건이 일치돼서, 다음 PC가 계산된 branch target address로 교체되는 것을, “branch is taken”이라고 표현한다. 아까 ALU에서 “Zero output”을 만들어 둔 것이 이때 사용된다. 두 값이 같은지 여부를 ALU가 출력할 때, 두 값의 차를 구하여 그 값이 0 이면, 즉, zero 아웃풋이 1로 출력되면 같다고 판단하기 떄문이다.
- jump instruction
28비트의 오프셋에 4배 혹은 shift left 2 연산을 하면 그만이다.
지금까지 언급한 datapath들을 모두 합하면 다음과 같다. 이 때, 두 개의 소스로부터 서로 다른 데이터가 온 상황에서 원하는 데이터를 선택하기 위해 multiplexor(Mux)를 사용한다.

4.4 A Simple Implementation Scheme
이제, 메인 컨트롤 유닛의 구현을 따라가보자.

- The ALU Control
ALU가 어떤 연산을 할지 입력 받는 4비트 컨트롤 인풋이 있었다. 비트 값에 따라 5개의 연산 중 하나로 동작한다.
이 4비트 컨트롤 인풋은 ALUOp라고 하는 2비트의 필드로부터 변환된다. ALUOp가 00인 LW, SW 연산에서는 ALU가 덧셈 연산을 해야하며, 01인 Beq 연산에서는 뺄셈 연산을 해야한다. 10인 R-type의 경우, 또 다른 필드인 Funct 필드를 추가로 확인해야한다. 우리는 ALU와 ALUOp에 어떤 비트가 들어왔을 때, 어떤 4비트 아웃풋을 만들어야 하는지 아래와 같은 truth table을 만들 수 있다. 우리의 메인 컨트롤 유닛이 구현해야할 명세다.

- instruction

우리는 3가지 종류의 인스트럭션을 보자. rs, rt, rd는 레지스터의 번호이다. rs는 항상 읽기에만 사용된다. rt는 대부분 읽기이지만, load 연산에 대해서는 쓰기로 사용된다. rd는 항상 쓰기에만 사용된다. 어찌됐든, 쓰기 레지스터는 인스트럭션 당 하나다. 그리고, address를 나타내는 16비트 오프셋은 sign-extend 연산을 필요로 한다. 종합하면, 아래처럼 인스트럭션은 분리되어 다음 회로에 전달된다.

ALUOp는 ALU control unit이 반환한다.

Control unit은 두가지 방법으로 구현된다. 첫번째 방법은 “microcoded control”이라고 하여, 컨트롤 유닛이 안에 작은 메모리를 갖고 연산을 하여 컨트롤 신호를 생성한다. 과거 CISC 프로세서들이 사용하던 방법으로 유연하지만 느리다. 반면 두번째 방법은 “hardwired control”로, combinational logic으로 신호를 생성한다. 빠르지만, 수정할수 없다. RISC 프로세서들이 사용한다.
4.5 An Overview of Pipelining
앞서 우리는 single-cycle 구현 방식을 사용했다. 모든 인스트럭션은 같은 길이의 클락 사이클 안에서 수행된다. 그러다 보니, 클락 사이클 주기는 가장 오래 걸리는 인스트럭션의 critical path에 의존하게 된다. 그래서 CPI(Cycle Per Instruction)가 1이여도, CC(Clock Cycle)이 길어져 전체적인 퍼포먼스가 떨어지게 된다. 그래서 나온 아이디어가 파이프라이닝(pipelining)이다.
우리는 인스트럭션의 실행과정을 다섯 단계로 나눈다.
- IFetch: Fetch Instruction from Memory
- Dec: Read Registers from the Decoded Instruction
- Exec: Execute the Operation.
- Mem: Access an Operand in Data Memory
- WB: Write the Result into a Register

빨래를 하는 과정을 생각해보자. 빨래는 4단계의 과정으로 수행된다. 1. 세탁하기. 2. 건조하기. 3. 개기. 4. 수납하기. 위 피규어의 첫번째 차트처럼 4번 수납하기 과정 후에야 1번 세탁하기 과정을 진행하는 것은 단순하다. 하지만, 네 과정이 일어나는 리소스가 세탁기, 건조기, 마룻바닥, 옷장으로 다 다르기에 네 과정을 동시에 수행해도 문제가 없다. 즉 피규어의 두번째 차트처럼 동시에 진행할 수 있다. 위 차트보다 아래의 파이프라이닝된 빨래 스케쥴링은 4배 더 빠르게 빨래들을 처리한다. 정확히는, 시작과 끝에 놀고 있는 리소스들 때문에 4배가 안되지만, 해야하는 빨래 양이 많아질수록, 이 효율은 4배에 가까워질것이다. 마찬가지로, 우리는 다섯 단계로 구성된 인스트럭션을 파이프라이닝할 수 있다.

빨래가 파이프라이닝이 가능했던 이유는 빨래의 4가지 스테이지가 일어나는 장소가 다르기 때문이였다. 하지만, 우리의 인스트럭션들은 안타깝게도 그렇지 않다. 아무 생각 없이 매번 다음, 다음, 다음 인스트럭션을 수행하면 문제가 생기는 상황이 생기는데 이를 해저드(hazard, 충돌)이라고 부른다. 3가지 종류가 있다.
구조적 해저드(Structural Hazard)
다행히 세탁기와 건조기가 별개의 기기였지만, 만약 두 과정이 같은 기기에서 일어난다면 우리는 파이프라이닝을 할 수 없었을 것이다. 안타깝게도 우리의 인스트럭션 실행과정에서 이런 일이 일어난다. 첫번째로, 첫단계인 IF(Instruction Fetch)와 Mem(Memory Access)과정은 모두 메모리 참조를 요구한다. 또, 2번째 과정인 ID(Instruction Decode)와 WB(Write Back)과정도 동시에 레지스터 파일로의 접근을 요구한다. 서로 다른 인스트럭션들이 동시에 같은 리소스를 요구하려 해서 발생하는 해저드를 구조적 해저드라고 부른다. 차차 이 문제를 해결해보겠다. 미리 스포일러를 하자면, IF와 MEM은 두개의 개념적 메모리로 분리하고, ID와 WB는 읽기/쓰기 연산을 시간적으로 분리한다.
데이터 해저드(Data Hazard)
데이터 해저드는 후속 인스트럭션이 앞선 인스트럭션을 기다렸다가 실행될수 있을때 발생한다. 아래의 인스트럭션을 보면 아래의 sub 인스트럭션 과정 수행에 add 인스트럭션의 결과인 $s0을 요구한다. 즉, sub 인스트럭션은 add 인스트럭션이 끝날때까지 기다려야 한다. sub 인스트럭션의 레지스터의 값을 읽기하는 ID 과정을 add 인스트럭션이 레지스터에 값을 쓰기하는 WB 과정이 끝날때까지 기다리려면 3번의 사이클을 기다리면(stall) 된다. 하지만, 이런 사례는 현실에서 굉장히 자주 발생하는 사례이다. 이럴때마다 매번 사이클들을 기다리기만 하며 보내는 것은 만족스럽지 않다.

내놓을 수 있는 다음 해결책은 포워딩(forwarding, or bypassing)이라고 부른다. add가 WB 과정까지 수행하고 나서야, sub의 ID가 실행되는 것은 너무 비효율적이다. add의 EX과정이 ALU에서 끝나자마자(덧셈 연산이 끝나자마자) 그 값을 sub 인스트럭션에서 넘겨 받는다면 좀 더 빠를 것이다. sub 인스트럭션이 일단 ID를 수행하고, 동시에 add는 EX 과정(덧셈)을 수행한다. 그리고, sub의 EX를 시작하기 전, 레지스터에서 읽어온 $s0 값 대신, add의 덧셈 결과를 덮어써서 연산에 이용한다면 파이프라이닝에 전혀 지연이 없다. 이것을 포워딩이라고 부른다. 아래의 피규어로 표현했다.

포워딩이 만능은 아니다. EX 단계 후 값을 포워딩할 수 있는 add와 달리, load 인스트럭션은 MEM 단계까지 끝나야 값을 포워딩해줄 수 있다. 그래서 어쩔수 없는 한 사이클의 기다림(stall)이 필요하다. 흔히 버블(bubble)로 아래의 그림처럼 표현한다.

컨트롤 해저드(Control Hazards)
해저드의 세번째 종류이다. 인스트럭션의 실행결과에 따라, 다음에 어떤 인스트럭션을 실행할지가 정해지는 경우이다. 해결할 수 있는 첫번째 방법은 이번에도 “기다리기”이다. IF 단계에서, 이번에 실행해야할 인스트럭션이 beq(branch if equal) 임을 알았다면, beq가 EX 단계을 수행해야, 다음에 실행할 인스트럭션을 알 수 있다. 그떄까지 기다리면 된다.
만약 새로운 장치를 추가해본다면 어떨까? 이 장치는 ID 단계에서 beq의 결과를 빠르게 연산해서, 다음 인스트럭션을 알려주는 장치이다. 그러면 아래의 피규어처럼 단 한사이클만 기다려도 충분히 다음 인스트럭션을 알아내어 fetch할 수 있다.

기다리기 방법 외에 또 다른 방법은 “예측하기”이다. 브랜치가 taken(바로 다음 인스트럭션이 아니라 다른 주소로 넘어감)되었는지, untaken되었는지를 예상하는 것이다. 만약 untaken 예측이 성공하면 우리는 최대 퍼포먼스로 파이프라이닝을 성공시킬 수 있다. (taken되는 경우는 여전히 기다림이 필요하다)

예측을 어떻게 할 수 있을까? 가장 흔한 방법은 과거의 인스트럭션 실행 결과를 참고하는 것이다. 이렇게 구현된 Dynamic branch predictors는 90% 이상의 정확도까지도 뽑아낼 수 있다. 4.8절에서 추가로 다룬다.
세번째 방법은 “결정 미루기”(delayed decision) 이다. beq와 같은 컨트롤 인스트럭션과 관련없는 인스트럭션을 수행하면서, 브랜치 결과를 지켜보는 것이다. 그림에서 beq 인스트럭션과 add 인스트럭션은 관련이 없다. 그래서 순서를 바꿔 beq인스트럭션을 먼저 실행하고, 그 결과를 기다리면서 add 인스트럭션을 수행해도 프로그램 실행 결과는 달라지지 않는다. 이 해법은 branch delay가 한 사이클 정도라면 굉장히 유용하기에, MIPS 아키텍쳐도 이 방법을 이용한다. 만약 delay가 더 길다면, 예측하기 방법이 대개 사용된다.
이렇게 4.5절 전체에서 파이프라이닝의 개념을 쭉 훑었다. 파이프라이닝은 병렬성(parellelism)을 이용한 테크닉이다. 다음 4.6, 4.7, 4.8, 4.9절에서는 앞서 훑어본 내용들을 더 깊게 파고든다.
4.6 Pipelined Datapath and Control

우리의 구조를 보자. 대부분의 데이터는 왼쪽에서 오른쪽으로 흐른다. 단, 위 피규어에서 파란색 화살표로 표시된 두 흐름은, 오른쪽에서 왼쪽으로 흐른다. 이 두 흐름은 각각 데이터 해저드와 컨트롤 해저드를 유발한다.
- WB 단계에서, 결과 데이터는 레지스터 파일로 보내진다.
- MEM 단계에서, 브랜칭 인스트럭션 실행 결과에 따라, 다음에 실행할 인스트럭션이 달라진다.

각 파이프라인을 완전히 독립적인 파이프라인으로 만들기 위해, 우리는 단계 사이사이의 상태를 저장할 레지스터가 필요하다. 빨래로 비유하자면, 세탁기에서 건조기, 건조기에서 마룻바닥, 마룻바닥에서 옷장으로 옷을 옮길 때 각 단계를 구분할 옷바구니가 필요하다. IF와 ID 사이에 필요한 레지스터 이름을 IF/ID라고 부른다. 마찬가지로, ID/EX, EX/MEM, MEM/WB 레지스터도 존재한다.

lw 인스트럭션 실행과정 중 쓰이는 리소스들을 파란색으로 표현했다. 리소스의 왼쪽 부분이 칠해진것은 쓰기 연산, 오른쪽이 칠해진것은 읽기 연산임을 의미한다. MEM 과정에서 읽기 연산을 수행하는 것을 볼 수 있다. 구체적으로는 아래와 같다. lw 인스트럭션을 기준으로 한다.
- IF: 인스트럭션 정보를 IF/ID 레지스터에 저장한다. PC 주소에 4를 더하여, PC에 스스로 업데이트하고, IF/ID 레지스터에도 남긴다. beq같은 브랜칭 인스트럭션에 쓰이는데, 일단 이 단계에서는 이 인스트럭션의 종류를 알 수 없기때문에 무조건 일단 저장해야한다.
- ID: IF/ID 레지스터로부터 16비트 상수나 읽을 레지스터의 번호를 넘겨받아, 상수의 부호 확장(sign extend)처리 및 레지스터 읽기 연산을 수행하여 ID/EX 레지스터에 넘긴다.
- EX: ID/EX 레지스터로 부터 받아온 피연산자(operands)들을 적절히 연산하여 EX/MEM 레지스터에 저장한다.
- MEM: EX/MEM으로부터 받은 결과(lw 인스트럭션에서는 읽을 메모리 주소)로 메모리에서 값을 읽어 MEM/WB 레지스터에 저장한다.
- WB: MEM/WB 레지스터에 저장된 값을 레지스터 파일에 쓰기한다.
sw 인스트럭션도 유사하나 주의할 점이 있다. ID 과정에서 얻어지는 쓰기 레지스터의 번호를 WB과정까지 잘 운반해주어야한다. IF/ID → ID/EX → EX/MEM → MEM/WB 까지.
마찬가지로 내리 운반해주어야할 값이 있다. Control signal들이다. 각 단계마다 필요한 Control signal들을 보자.
- IF: 없음(매번 하는일이 같다.)
- ID: 없음(매번 하는일이 같다.)
- EX: RegDst(목적지 레지스터가 rt인지 rd인지), ALUOp(ALU에서 어떤 연산을 할지), ALUSrc(ALU에 들어갈 피연산자가 rt인지 상수인지)
- MEM: MemRead(할일이 메모리 읽기인지 아닌지), MemWrite(할일이 메모리 쓰기인지 아닌지), Branch(브랜치 명령어인지), PCSrc(taken인지 아닌지)
- WB: MemtoReg(메모리에 쓰기 할게 메모리에서 읽은 값인지, 읽기 전 주소인지), Reg-Write(레지스터에 쓰기 할지말지)
이렇게 그룹지어진 Control signal들은 IF/ID 레지스터부터 내리 운반된다.

4.7 Data Hazards: Forwarding versus Stalling
데이터 하자드에 대해 면밀히 살펴보자. 데이터 하자드의 정확한 발생 조건은 무엇인가?

왼쪽의 다섯개의 인스트럭션 시퀀스를 실행하는 모습이다. $2 레지스터에는 원래 10이 담겨있었으나, 첫번째 인스트럭션의 실행 결과로, 5번째 사이클에서 -20으로 값이 달라졌다. 그다음 두번째 and 인스트럭션은 -20을 가지고 연산을 수행해야하지만, 레지스터에서 값을 읽은 3번째 사이클 때에는 $2의 값은 10이였다. 데이터 하자드가 발생했다.
이 그림을 통해 이해할 수 있는것은, 앞선 인스트럭션의 WB 단계 이전에 이후 인스트럭션의 EX 단계가 실행될때, 문제가 발생한다는 것이다. 다르게 말하면, EX/MEM 혹은 MEM/WB 레지스터에 들어있는 rd 레지스터 넘버가, ID/EX 레지스터의 rs 혹은 rt 레지스터의 넘버와 같을 때 데이터 하자드가 일어남을 알 수 있다. 이 내용을 수식적으로 표현하면 아래와 같다.

지금까지 추론한 데이터 하자드의 발생 조건이지만, 아직 완전하지는 않다. 왜냐하면 모든 인스트럭션이 쓰기작업을 요하지는 않기 때문이다. 우리는 이것을 WB 단계의 컨트롤 신호인 RegWrite의 값으로 알 수 있다. 쓰기 작업을 요하는 인스트럭션은 RegWrite 값이 1이다.
또, $0 레지스터는 항상 0의 값을 유지한다. 이 레지스터에는 쓰기 작업도 할 수 없고, 항상 0임을 보장하는 특이한 레지스터이다. 만약 “sll $0, $1, 2” 처럼 쓰기할 레지스터가 $0인 경우는 하자드를 걱정할 필요 없다.
회로로 보자면, 아래의 그림과 같을 것이다. MEM/WB와 EX/MEM 레지스터로부터 나온 RegisterRd 값은, forwarding unit으로 넘겨진다. 이 forwarding unit은 rs와 rt 값을 다루는 두개의 Mux에 ForwardA, ForwardB라는 컨트롤 신호를 보내게 된다. 레지스터에서 읽어온 rs/rt값과 이전 인스트럭션이 연산하여 포워딩해준 rd값 중 어떤값을 연산에 사용할지 정할 수 있는 것이다. ForwardA, B는 2비트 신호 이다. 00, 10, 01은 각각 ALU에 넘겨줄 값을 ID/EX, EX/MEM, MEM/WB에서 가져온 값으로 이용함을 의미한다.


또 다른 복잡한 상황은, EX/MEM과 MEM/WB에서 포워딩된 두 값이 다른 상황이다. 오른쪽과 같은 시퀀스에서 이런 일이 발생할 수 있다. EX/MEM.RegisterRd, MEM/WB.RegisterRd, ID/EX.RegisterRs 셋의 값이 모두 동일한 것이다. 이럴때는 더 이후에 연산된 값(두번째 add의 계산 결과), 즉, EX/MEM.RegisterRd에 더 최신의 $1 값이 들어있다. 따라서, EX/MEM.RegisterRd의 포워딩은 EX/MEM.RegisterRd와 MEM/WB.RegisterRd의 값이 같은 상황에서는 일어나면 안된다.
위 내용들을 모두 종합하면, 아래의 로직으로 forwarding unit이 ForwardA, ForwardB의 값을 결정한다는 것을 알 수 있다.

4.5절에서 이미 다뤘듯이, load 인스트럭션의 경우라면 얘기가 또 다르다. Load 연산은 MEM 과정이 지나고서야 쓰기할 값이 정해지므로, 한 번의 stall이 필연적이다. 아래의 그림을 통해 알 수 있듯이, load의 경우는 포워딩만으로도 문제를 해결할 수 없다.

그래서 우리는 forwarding unit에 이어서, hazard detection unit을 추가할 것이다. 이것은 ID 단계에서 동작하며, load 연산과 그 다음 load된 값을 사용하는 연산 사이에 stall을 추가한다. 조건은 아래와 같을 것이다.

and 연산의 ID 수행 이전에, 위 조건을 체크한다. 앞선 인스트럭션의 MemRead가 1이였다면, 즉 메모리를 읽는 load 연산이였다면, 그리고, 해당 load 연산 결과가 담기는 레지스터 rt와 내가 이번 add 연산에 사용할 rs, rt의 번호가 같다면, 해당 파이프라인을 stall 한다.
ID 단계가 stall 되면, 그 이전의 IF 단계도 당연히 stall 되어야 한다. PC 레지스터와 IF/ID 레지스터의 값을 갱신하지 않고 그대로 두면, 간단하게 stall은 구현된다. 비유하자면, 이미 세탁기와 건조기에 들어가있는 빨래를, 아무것도 건들지 않고 다시 세탁기와 건조기를 재가동하는 셈이다. 아무 효과도 없는 이런 인스트럭션을 nops라고 부른다. 그림으로 표현하자면 아래와 같다.

4.8 Control Hazards
언젠가 작성됩니다..
4.9 Exceptions
언젠가 작성됩니다..
4.10 Parallelism via Instructions
파이프라이닝은 인스트럭션은 병렬적으로 실행할 수 있게 한다. 이렇게 인스트럭션이 병렬적으로 실행되는것을 ILP(Instruction-level parellelism)이라고 한다. 병렬성을 높이는 두 가지 방법을 소개한다.
첫번째는 파이프라이닝의 단계를 늘리는 것이다. 빨래로 비유하면, “세탁” 과정을 “씻기”,”린스”,”헹구기” 3가지 과정으로 쪼개서, 4단계의 파이프라인을 6단계의 파이프라인으로 늘리는 것이다. 이후, 6개의 단계들의 실행시간을 재분배(rebalance)하여 클락 사이클을 줄여, 성능의 개선을 이뤄낼 수 있다.
두번째는, 파이프라이닝에 사용되는 하드웨어 구성요소들을 복제하여, 동시에 여러개의 인스트럭션을 실행할 수 있도록 하는 것이다. 이 기술을 다중 내보내기(multiple issue)라고 한다. 다시 빨래로 비유하면, 원래 세탁기 하나, 건조기 하나가 있던 것을, 세탁기 3대, 건조기 3대로 늘려서 3배 많은 일을 하도록 하는 것이다. 이 방식의 단점이라고 하면, 모든 기계를 최대한 바쁘게 가동하고, 파이프라인 단계 간 결과물을 옮기는데에 추가적인 작업이 든다는 것이다.
한 단계 안에서 여러 인스트럭션을 실행하는 일은, CPI를 1보다 낮출 수 있다. CPI(Clock cycle per instruction) 대신 IPC(Instruction per clock cycle)을 사용하는 것이 유용할수도 있다. 예를 들어, 4GHz 4-way multiple issue 프로세서는 1초당 160억 개의 인스터럭션을 최대 0.25의 CPI, 혹은 4의 IPC의 성능으로 실행해낸다.
Multiple issue를 구현하는 두 가지 방식이 있다. 동적(dynmaic)인 방식과 정적(static)인 방식이다. 아래의 두 관점이 있다.
- 인스트럭션을 이슈 슬롯(issue slot)에 어떻게 배치할지: 컴파일러가 컴파일 시에 미리 정하는 정적인 방식과, 런타임 시에 결정하는 동적인 방식
- 컨트롤/데이터 해저드 대응: 동적인 방식은 정적인 방식 대비 하드웨어적인 기술을 이융하여 몇몇 종류의 해저드들을 완화한다.
ILP에서 중요한 개념 중 하나는 추측(speculation)이다. 분기(branch) 결과를 미리 추측하면 분기 이후의 명령어들을 더 빨리 실행할 수 있다. 앞선 store와 뒤따르는 load가 서로 다른 주소를 사용하는 것을 추측하면, load를 store보다 먼저 실행할 수도 있다. 추측은 성능을 크게 향상시킬 수 있지만, 잘못 사용하면 오히려 성능 저하나 예외 발생 등 문제를 일으킬 수 있으므로, 신중한 적용이 중요하다. 추측이 틀릴 경우를 대비해, 결과를 검증하고 잘못된 경우 효과를 되돌리는 복구 메커니즘이 반드시 필요하며, 이는 소프트웨어와 하드웨어에서 각각 다르게 구현된다.
언젠가 작성됩니다..
'개인 공부' 카테고리의 다른 글
| [오토마타] 1. 유한 결정적 오토마타(DFA, Deterministic Finite Automata) (0) | 2025.09.29 |
|---|---|
| [전산기조직] 메모리 계층 구조(Memory Hierarchy) (1) | 2025.06.16 |
| [데이터베이스 개론] 8. 쿼리 실행(Query Execution) (0) | 2025.06.16 |
| [데이터베이스 개론] 7. 인덱스 구조(Index Structures) (0) | 2025.06.16 |
| [데이터베이스 개론] 6. 2차 저장소 관리(Secondary Storage Management) (0) | 2025.06.16 |