---
# System prepended metadata

title: 第 5-3 講 Pipeline3
tags: [計算機結構]

---

{%hackmd r14yVks8p %}

# 背景 Instruction-Level Parallelism

管線的效果不錯，但我們希望更厲害，所以：

- 可以用更深的管線，也就是切更細
    - 但是越切越細頻率會越高，功耗炸裂
- 另一個方法，也是待會的主角，叫做 Multiple issue
    - 讓沒有相依性，或是相依性較小的指令放在同個 cycle 一起運行，下面會講細節
    - 這樣可以使得 CPI 小於 1

# Multiple issue

分為靜態跟動態兩種。

:::warning
下列 by Gemini
:::

兩種主要的 Multiple Issue 處理器類型
--------------------------

Multiple Issue 的實現在很大程度上取決於**誰**來決定哪些指令可以同時發行：是**硬體**還是**編譯器**。

### 1. **動態多重發行處理器 (Dynamic Multiple-Issue Processors) / 超純量 (Superscalar)**

-   **誰決定？** **硬體**在程式執行時 (Run-time) 動態決定。
-   **發行方式：** 處理器會從指令串流中抓取多條指令，並在執行時檢查它們之間是否存在**資料相依性 (Data Dependencies)** 或**結構相依性 (Structural Hazards)**。
    -   如果指令之間**沒有**相依性，硬體就會將它們分派給空閒的功能單元，並允許它們**亂序執行 (Out-of-Order Execution)**，但保證**按序完成 (In-Order Completion)**。 
-   **優點：** 程式設計師或編譯器不需擔心指令排程；適用於現有的程式碼。
-   **範例：** 大多數現代伺服器、桌上型電腦和高效能行動處理器。

### 2. **靜態多重發行處理器 (Static Multiple-Issue Processors) / 超長指令字 (VLIW, Very Long Instruction Word)**

-   **誰決定？** **編譯器**在程式編譯時 (Compile-time) 靜態決定。
-   **發行方式：** 編譯器會將一組可以平行執行的指令打包成一個**超長指令字**，並在程式碼中明確指示哪些指令要同時執行。
-   **優點：** 硬體設計相對簡單，不需要複雜的亂序執行電路。
-   **缺點：** 對編譯器要求很高，如果程式碼的 ILP 不足，會浪費指令空間（用 NOP 填充）；硬體一旦改變，可能需要重新編譯。    
-   **範例：** 某些數位訊號處理器 (DSP) 和嵌入式系統。



# Static multiple issue

靜態是編譯器負責的，由他判斷指令間的相依性，讓他們在時序上是相同的，同個 cycle 會在同個 stage。

這些被放在一起的指令會被放入叫做「Issue slot」或「Issue packets」裡面，代表他們是一起的。
>`n` 個指令放在一起的話 Issue packets 就有 `4n` Bytes 這麼大。

因為還是有可能會發生相依性，所以編譯器也要知道何時放 nop
>會因為 ISA 而有所不同，而編譯器都要知道，編譯器 yyds

# RISCV Static Dual Issue

下面用 Dual Issue 示範 Static Issue。

## Issue Rule

一個 Issue 重要的是他的「rule」，規定哪些指令可以放一起，哪些不行。

制定 Rule 時要去評估要給多大的彈性，哪幾種指令可以放一起；如果是任意放的話，需要做調整的硬體也會越複雜，這樣反而 cost 會變高，本末倒置。

這裡 Dual Issue 的規則是：

- 一個只能是 ALU/branch 指令
- 一個只能是 load/store 指令

## 電路圖

下圖是根據 Dual Issue Rule 所修改的電路圖：

<iframe src="https://drive.google.com/file/d/1BoXM4Azb84LJ5tGIYy2-ryFECn_sLy7U/preview"  height="580"></iframe>

>這個電路圖有個很迷的地方，為甚麼 `ALU/branch` 的 `RS1` 會有 MUX 做判斷。

- 在 IF 的階段，因為要同時執行兩個指令，所以線路要複製一份出來。
- 在 ID 階段：
    - `ALU/branch` 因為只是單純計算，不需要用到記憶體，所以 ALU 的結果不會通過記憶體而是直接拉回來
    - 並且可以看到 Branch 的操作統一拉到 `Decode` 的部分了。
    - `load/store` 因為只需要計算地址，並且是從 IMM 來，所以直接拉 IMM 到 ALU，最後直接拉進記憶體。


##  Aggressive Scheduling

因為讓兩條指令有相同的時序，所以如果發生 Data Hazard，就不能做 forwarding 來解決，就必須要讓一條指令等另一條，這相當於一個 stall。
遇到 [load-use data hazard](https://hackmd.io/6N6TSxpxTSu_p7C5cDgLxg?view#Load-use-data-hazard)，一樣靠 stall 1 cycle 解決。

所以想要避免 stall 唯一的手段就是更積極的重新排序。如果重新排序也解決不了，則該 packets 就會有一邊是 nop，只能浪費掉了。

下面是一個跟地址操作有關的例子：

```ASM=
Loop: ld   x31, 0(x20)     // x31=array element
      add  x31, x31,x21    // add scalar in x21
      sd   x31, 0(x20)     // store result
      addi x20, x20,-8     // decrement pointer
      blt  x22, x20,Loop   // branch if x22 < x20
```

填進 packets 表格如下：



|               |                ALU/Branch                |                  Load/Store                   | Cycle |
|:-------------:|:----------------------------------------:|:---------------------------------------------:|:-----:|
| $\text{Loop}$ |               $\text{nop}$               |    $\color{yellow}{\text{ld   x31, 0(x20)}}$    |   1   |
|               |        $\text{addi x20, x20,-8}$         |                 $\text{nop}$                  |   2   |
|               | $\color{yellow}{\text{add  x31, x31,x21}}$ |                 $\text{nop}$                  |   3   |
|               |       $\text{blt  x22, x20,Loop}$        | $\text{sd   x31, }\color{red}{8}\text{(x20)}$ |   4   |

- 首先看==黃色的==，因為第一條 `ld` 跟第二條 `add` 發生了 load-use data hazard，所以必須等一個 cycle，因此他們兩個才會相隔一個 cycle。
- 再來，可以很神奇地發現，第 4 行的 `addi` 被往上提了
    - 因為 `addi` 是在將地址 -8，我希望他可以早做一些，所以就大膽地給他放在第 2 個 cycle
    - 只要將第 3 行的 `sd` 中儲存的地址，往回移動 8 就可以存到正確的地址了
    - 這樣一來就可以壓縮 cycle 數了，跟鬼一樣
    - 不然原本 4、5 行會放在第 5 跟第 6 個 cycle

:::warning
上面的 IPC(Instruction Per Cycle) 是 $\frac{5}{4}=1.25$，但是理論上 Dual Issue 最佳 IPC 應該是 2，也就是每個 cycle 應該要可以執行兩個指令。
:::

## Loop Unrolling & Renaming

如果上面的例子，實際上是個迴圈，此時我們的編譯器更積極的去檢查程式碼做了甚麼。

```asm=
Loop:     ld   x31, 0(x20)     // x31=array element
          add  x31, x31,x21    // add scalar in x21
          sd   x31, 0(x20)     // store result
          addi x20, x20,-8     // decrement pointer
          blt  x22, x20,Loop   // branch if x22 < x20
```

- 因為上面的 code 就只是跑個迴圈，單純的把某個陣列內的每個值加上一個常數
- 所以聰明的編譯器就打算一次做四次同樣的操作，這樣就不需要每次都經過 `blt` 判斷
   - 但不一定做的次數都是 4 的倍數，所以編譯器會額外加邊界處理的 code，這裡不提這個
- 然後這些指令就可以壓縮在一起做了

所以編譯器將指令稍微修改後，就會變成這樣：

```asm=
Loop:   ld   x31, 0(x20)
        add  x31, x31, x21
        sd   x31, 0(x20)
        
        ld   x31, -8(x20)
        add  x31, x31, x21
        sd   x31, -8(x20)
        
        ld   x31, -16(x20)
        add  x31, x31, x21
        sd   x31, -16(x20)
        
        ld   x31, -24(x20)
        add  x31, x31, x21
        sd   x31, -24(x20)
        
        addi x20, x20,–32
        blt  x22, x20,Loop
```

>可以看到就直接從對應地址拿值出來改。

- 但是這樣會遇到一個問題，因為修改後的值都「暫時放在」 `x31` 這個相同的 REG 裡面，會造成所謂的 name dependency
- 所以聰明的編譯器知道說，既然是暫存用的，那我就給每個人放在不同的 REG 就好了
- 這個操作就叫做「**Renaming**」；編譯器分別用了 `x31` `x30` `x29` `x28`。

```asm=
Loop:   ld   x31, 0(x20)
        add  x31, x31, x21
        sd   x31, 0(x20)
        
        ld   x30, -8(x20)
        add  x30, x30, x21
        sd   x30, -8(x20)
        
        ld   x29, -16(x20)
        add  x29, x29, x21
        sd   x29, -16(x20)
        
        ld   x28, -24(x20)
        add  x28, x28, x21
        sd   x28, -24(x20)
        
        addi x20, x20,–32
        blt  x22, x20,Loop
```

最後再將這些指令放到 packets 表格：

|               |                ALU/Branch                |                   Load/Store                    | Cycle |
|:-------------:|:----------------------------------------:|:-----------------------------------------------:|:-----:|
| $\text{Loop}$ |        $\text{addi x20, x20,-32}$        | $\text{ld   x31, }\color{green}{0}\text{(x20)}$ |   1   |
|               |               $\text{nop}$               | $\text{ld   x30, }\color{red}{24}\text{(x20)}$  |   2   |
|               | $\color{yellow}{\text{add  x31, x31,x21}}$ | $\text{ld   x29, }\color{red}{16}\text{(x20)}$  |   3   |
|               | $\color{yellow}{\text{add  x30, x30,x21}}$ |  $\text{ld   x28, }\color{red}{8}\text{(x20)}$  |   4   |
|               | $\color{yellow}{\text{add  x29, x29,x21}}$ | $\text{sd   x31, }\color{red}{32}\text{(x20)}$  |   5   |
|               | $\color{yellow}{\text{add  x28, x28,x21}}$ | $\text{sd   x30, }\color{red}{24}\text{(x20)}$  |   6   |
|               |               $\text{nop}$               | $\text{sd   x29, }\color{red}{16}\text{(x20)}$  |   7   |
|               |       $\text{blt  x22, x20,Loop}$        |  $\text{sd   x28, }\color{red}{8}\text{(x20)}$  |   8   |

上面的表格跟原本的稍為不一樣，原因跟上面的第一個版本一樣，聰明的編譯器想要讓 `addi` 一開始就執行，也就是原本 17 行的指令被移到第一行，所以一開始的 `x20` 就已經被往後 32 `btyes` 了，因此原本 `ld` 跟 `sd` 的地址都要進行修改。

而第一個 `ld` 因為跟 `addi` 是同個 packet，所以他還不會受地址的影響，因此地址依舊是綠色的 0。

:::warning
爆改後的 IPC(Instruction Per Cycle) 是 $\frac{14}{8}=1.75$，很接近理論值 2 了，但是付出的代價是使用更多 REG 跟記憶體來儲存修改過的新的指令。
:::

# Dynamic multiple issue

有個專門的硬體叫 Out of order unit 會去判斷當前指令能不能同時序執行。

他會將指令讀取後去做分類，判斷當前指令是哪種操作；然後沒有相依性的就可以先算出結果，所以是沒有順序的 Out of Order。

但是最後寫回記憶體或暫存器時就會依照正確的順序把值寫入。

:::warning
會需要 Dynamic 的原因是，Static 的其實會比較保守，無法做到更多地步，因為：

- 實際上的 code 會因為一堆原因而不是那麼好 predicable
- branch 相關指令很難做 Schedule
- 不同 ISA 的硬體會不一樣

因此還是需要有硬體，在第一現場做判斷。
:::

# Multiple Issue 功效

Multiple Issue 確實有效，但是也還是有他的極限，越多 Issue，得到的回報會越來越少。

# Power Efficiency

額外的硬體，就需要功耗。

除了上面的 Dynamic Multiple Issue，還有一個輔助他的傢伙叫 speculations，負責猜的，他也需要電。

所以統整來說，Power 會受下面幾項影響

- Clock Rate
- Pipeline Stages
- Issue Width
- Out-of-order&Speculation
- Cores

所以要看不同的規格在不同的 benchmark 有何種表現，去做取捨。

# 不同的設計


伺服器級別的 CPU ，因為他的使用場景是為了應付大量，但性質相似且簡單的任務，所以通常會具有高核心，時脈低(pipeline 數少)，以及 issue width 不會很高等特性。


---

## 回顧

- Multiple Issue 操作
- Loop Unrolling 操作