<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Issue on 0xc0de</title>
    <link>https://0xc0de.xyz/tags/issue/</link>
    <description>Recent content in Issue on 0xc0de</description>
    <image>
      <title>0xc0de</title>
      <url>https://0xc0de.xyz/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</url>
      <link>https://0xc0de.xyz/%3Clink%20or%20path%20of%20image%20for%20opengraph,%20twitter-cards%3E</link>
    </image>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <lastBuildDate>Sat, 10 May 2025 00:00:00 +0800</lastBuildDate>
    <atom:link href="https://0xc0de.xyz/tags/issue/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>&lt;超標量處理器概覽&gt; 第 11 章 - 真實世界的例子：Alpha 21264 處理器</title>
      <link>https://0xc0de.xyz/posts/superscalar-overview-ch11/</link>
      <pubDate>Sat, 10 May 2025 00:00:00 +0800</pubDate>
      <guid>https://0xc0de.xyz/posts/superscalar-overview-ch11/</guid>
      <description>&lt;h1 id=&#34;111----概述&#34;&gt;11.1 -  概述&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;DEC 的 Alpha 21264 是 superscalar CPU 的一個典範，其為 4-way out-of-order superscalar CPU，工作頻率是 466 ~ 667 MHz，benchmark：SPECint95 - 40、SPECfp95 - 86；與同時期其他的處理器 benchmark 對比：&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;image.png&#34; loading=&#34;lazy&#34; src=&#34;https://0xc0de.xyz/posts/superscalar-overview-ch11/image.png&#34;&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Alpha 21264 的 pipeline 最多同時支持 80 條指令，以及 80 個 checkpoints，因此對於 branch mis-prediction、exception、interrupt，Alpha 21264 都可以快速地恢復 CPU 狀態&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Alpha 21264 的分支預測器實現了基於局部歷史 (Local prediction) 和基於全局歷史 (Global prediction) 兩種預測方式，並根據程式的執行情況，動態選擇預測率最高的方法，這就像是兩種分支預測方式在進行競爭一樣&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;對於 load/store 指令，Alpha 21264 採用了 Speculative Memory Disambiguation 和 Load hit/miss Prediction&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Alpha 21264 採用 7-stage pipeline，比較短的 pipeline 使 branch mis-prediction 發生時的 mis-penalty 比較低，從而提高處理器的效率：&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="111----概述">11.1 -  概述</h1>
<ul>
<li>
<p>DEC 的 Alpha 21264 是 superscalar CPU 的一個典範，其為 4-way out-of-order superscalar CPU，工作頻率是 466 ~ 667 MHz，benchmark：SPECint95 - 40、SPECfp95 - 86；與同時期其他的處理器 benchmark 對比：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image.png"></p>
</li>
<li>
<p>Alpha 21264 的 pipeline 最多同時支持 80 條指令，以及 80 個 checkpoints，因此對於 branch mis-prediction、exception、interrupt，Alpha 21264 都可以快速地恢復 CPU 狀態</p>
</li>
<li>
<p>Alpha 21264 的分支預測器實現了基於局部歷史 (Local prediction) 和基於全局歷史 (Global prediction) 兩種預測方式，並根據程式的執行情況，動態選擇預測率最高的方法，這就像是兩種分支預測方式在進行競爭一樣</p>
</li>
<li>
<p>對於 load/store 指令，Alpha 21264 採用了 Speculative Memory Disambiguation 和 Load hit/miss Prediction</p>
</li>
<li>
<p>Alpha 21264 採用 7-stage pipeline，比較短的 pipeline 使 branch mis-prediction 發生時的 mis-penalty 比較低，從而提高處理器的效率：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%201.png"></p>
</li>
<li>
<p>Alpha 21264 所使用的設計理念是領先於那個時代的，包含競爭的分支預測、store/load 指令間的相關性預測，數量豐富的 checkpoints 等，它的設計理念被用到了未來的 AMD 和 Intel CPU 中，直接或間接地影響了現代電腦產業</p>
</li>
</ul>
<h1 id="112---取指令和分支預測">11.2 - 取指令和分支預測</h1>
<ul>
<li>Alpha 21264 每個 cycle 可以從 I-Cache 中讀取四條指令進 pipeline，其 I-Cache 的 size 為 64 KB、採用 2-way associative，並使用了 2 個 stages 來讀取 I-Cache：<code>fetch0</code> 和 <code>fetch1</code>
<ul>
<li><code>fetch0</code>：讀取 I-Cache，但這個 cycle 只能得到 I-Cache 的指令 (i.e. data) 和 tag，無法再做其他的事情</li>
<li><code>fetch1</code>：進行 tag 比對的同時，還會將指令送到 decoder 和 register renaming 相關的元件
<ul>
<li>在 Alpha 21264 中，這個傳輸需要跨越大半個晶片，比較花時間，這也是 Alpha 21264 使用了 2 個 stages 來讀取 I-Cache 的原因</li>
</ul>
</li>
</ul>
</li>
<li>Alpha 21264 每個 cycle 都需要向 I-Cache 發送位址來讀取指令，為了盡早察覺到分支指令和分支指令的目標位址，Alpha 21264 透過兩個方法來提昇 instruction fetch 的準確度：
<ul>
<li>line/way 的預測 - 較簡單的分支預測器，可以在 <code>fetch0</code> 就得到結果，但是預測準確度比較低</li>
<li>分支預測 - 較複雜的分支預測器，需使用 2 個 cycles，也就是 <code>fetch1</code> 才能得到結果，但是預測準確度比較高
<ul>
<li>如果發現 <code>fetch0</code> 的預測結果與 <code>fetch1</code> 的預測結果不符，則會拋棄 <code>fetch0</code> 的預測結果，使用 <code>fetch1</code> 的預測結果來 fetch instruction，但也會因此產生 1 個 cycle 的 bubble
<ul>
<li>i.e. 使用 <code>fetch0</code> 的預測結果讀進來的 instruction 會被 flushed 掉</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="1121---lineway-的預測">11.2.1 - line/way 的預測</h2>
<ul>
<li>
<p>Alpha 21264 在 fetch stage，為了可以盡快地從 I-Cache fetch 指令，對 I-Cache 的 instruction fetch 位址也進行了預測，這就是 <strong>line/way 預測</strong></p>
<ul>
<li>這種方法本質上就是將 BTB 放進了 I-Cache 中</li>
</ul>
</li>
<li>
<p>由於 Alpha 21264 每個 cycle fetch 的四條指令必須是 4 words aligned 的，因此只需要對每條 cache line 中每一組 4 words aligned 的四條指令使用一個 line/way 預測即可：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%202.png"></p>
<ul>
<li>對於一條 64 bytes 的 cache line，需要使用 4 個 line/way 預測 (64 bytes / (4 words * 4 bytes/word) = 4)，每個 line/way 預測包含了以下的資訊：
<ul>
<li><code>Successor way</code>：
<ul>
<li>下一個 cycle 要取的 fetch group，位在 I-Cache 的那個 way
<ul>
<li>E.g. 4-way set-associative I-Cache，共需要 2 bits 來表示</li>
</ul>
</li>
</ul>
</li>
<li><code>Successor index</code>：
<ul>
<li>下一個 cycle 要取的 fetch group，第一條指令在 cache line 中的位置
<ul>
<li>E.g. 一個 64 KB、2-way set-associative cache、cache line 為 64 bytes 的 I-Cache：
<ul>
<li>需使用 <code>PC[14:6]</code> ⇒ <strong>9 bits</strong> (64 KB / 2-way set-associative / 64 byes = 512 bytes) 來找到一條 cache line</li>
<li>需使用 <code>PC[5:2]</code> ⇒ <strong>4 bits</strong> (64 bytes / 4 bytes/word ⇒ 16，一條 cache line 中共有 16 個 words) 來找到 cache line 中的某個 word</li>
<li>因此，<code>Successor index</code> 只需儲存 <code>PC[14:2]</code> ⇒ <strong>13 bits</strong> 即可</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><code>Branch position</code>：
<ul>
<li>此 cycle 所取出的 fetch group 中，如果存在分支指令，則將這條分支指令的下一條指令的位址資訊記錄在 <code>branch position</code> 欄位</li>
<li>之所以不記錄分支指令本身的位址資訊，而是其下一條指令的位址資訊，是因為如果此 cycle 取出的 fetch group 中存在預測會跳轉的分支指令，則它後面的指令都不會進入 pipeline，但分支指令本身是會進入 pipeline 的
<ul>
<li>因此記錄分支指令的下一條指令的位址資訊更容易找到此 cycle 所取出的 fetch group 中，哪些指令不應該進入 pipeline</li>
<li>當此 cycle 所取出的 fetch group 中不存在分支指令，或是存在預測結果為不跳轉的分支指令時，只需將 <code>branch position</code> 寫入 <code>2’b00</code>，就可以表示 “此 cycle 取出的 fetch group 中的所有指令，都需要進 pipeline” 了</li>
<li>如果預測跳轉的分支指令是這個 fetch group 的最後一條指令，那麼這個 fetch group 中的每條指令也都應該進 pipeline，同樣也只需將 <code>branch position</code> 寫入 <code>2’b00</code> 即可</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>在 Alpha 21264 中，當一個 cache line 從 L2 Cache 讀進 L1 I-Cache 時，這條 cache line 包含的所有預測資訊都會被初始化為“沒有需要跳轉的分支指令”，此時 line/way 的預測資訊會指向下一個 PC 值，i.e. <code>PC + sizeof(fetch group)</code></p>
<ul>
<li>一旦在後面的過程中發現這個預測資訊是錯的 (e.g. <code>fetch1</code> 的預測結果與 <code>fetch0</code> 的預測結果不同)，就會修改 cache line 中的所記錄的預測資訊</li>
<li>可以按照之前 <code>2-bit saturating counter</code> 的方式來管理 line/way 的預測值，也就是只有當連續兩次預測失敗時，line/way 的預測值才會改變</li>
</ul>
</li>
<li>
<p>每個 cycle 都會依據目前 fetch group 的 line/way 的預測器，決定下個 cycle 要 fetch 的指令位址</p>
</li>
<li>
<p>範例：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%203.png"></p>
<ul>
<li>64 KB I-Cache，2-way set-associative，每條 cache line 是 32 bytes (i.e. 32 bytes / 4 bytes/word = 8 words)，每 4 個 words 使用一個 line/way 預測器，因此每條 cache line 都包含了 2 個 line/way 預測器
<ul>
<li>可以用 <code>PC[14:2]</code> 從 I-Cache 中找到需要的指令</li>
<li>Cycle 0：
<ul>
<li><code>PC = 0x20580354</code>，對應到 way 0；由於每個 cycle fetch 的四條指令必須是 4 words aligned 的，因此只能讀出三條指令</li>
<li><code>Branch position = 2’b00</code>，因此這三條指令都會進 pipeline
<ul>
<li>i.e. A0 ~ A2 會進 pipeline</li>
</ul>
</li>
<li><code>Successor index = 0x360</code>，因此下一個要 fetch 的指令位址：<code>0x20580360</code></li>
</ul>
</li>
<li>Cycle 1：
<ul>
<li><code>PC = 0x20580360</code>，對應到 way 1；由於是 4-word aligned 的，因此四條指令都可以被讀出</li>
<li><code>Branch prediction = 2’b11</code>，因此最後一條指令不會進 pipeline
<ul>
<li>i.e. A3 ~ A5 會進 pipeline</li>
</ul>
</li>
<li><code>Successor index = 0xa28</code>，因此下一個要 fetch 的指令位址：<code>0x20580a28</code></li>
</ul>
</li>
<li>Cycle 2：
<ul>
<li>PC = <code>0x20580a28</code>，對應到 way 1；由於每個 cycle fetch 的四條指令必須是 4 words aligned 的，因此只能讀出兩條指令</li>
<li><code>Branch position = 2’b00</code>，因此這兩條指令都會進 pipeline
<ul>
<li>i.e. B0 ~ B1 會進 pipeline</li>
</ul>
</li>
<li><code>Successor index = 0xa30</code>，因此下一個要 fetch 的指令位址：<code>0x20580a30</code></li>
</ul>
</li>
<li>Cycle 3：
<ul>
<li><code>PC = 0x20580a30</code>，對應到 way 1；由於是 4-word aligned 的，因此四條指令都可以被讀出</li>
<li><code>Branch prediction = 2’b11</code>，因此最後一條指令不會進 pipeline
<ul>
<li>i.e. B2 ~ B4 會進 pipeline</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Line/way 預測方法相當於將 BTB 放到了 I-Cache 中，然後相較於獨立的 BTB：</p>
<ul>
<li>缺點：
<ul>
<li>每條 cache line 都要使用固定個數的預測資訊，例如上述範例，每條 cache line 都包含了 2 個 line/way 預測器：32 bytes cache line / (4 words * 4 bytes/word)) = 2
<ul>
<li>然而很多 cache line 中其實並沒有分支指令，浪費硬體空間</li>
<li>獨立的 BTB 不會有這個問題，因為它只會將預測結果為跳轉的分支指令放進 BTB 中</li>
</ul>
</li>
<li>隨著 cache line size 的增大，所需要的 line/way 預測器的個數也會跟著增多
<ul>
<li>E.g. 64 bytes cache line 需要使用 4 個 line/way 預測器</li>
<li>獨立的 BTB size 都是固定的，不會隨著 cache line size 的變化而改變</li>
</ul>
</li>
<li>每當一條 cache line 被 replace 時，它的 line/way 預測資訊會被預設值所取代
<ul>
<li>預設值 ⇒  <code>successor index</code> 會指到下一道 PC address</li>
<li>獨立的 BTB 則與 cache replacement 無關</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>在 fetch1 stage 會驗證 fetch0 stage 的預測結果是否正確</p>
<ul>
<li>將 cache 的 tag (i.e. <code>PC[47:15]</code>) 與 successor index (i.e. <code>PC[14:2]</code>) 合併，就可以組成使用 line/way 預測的 PC address，在 fetch 1 stage，就可以與使用分支預測的預測結果相比，如果 PC address 不同，就代表 line/way 的預測錯誤，就會改使用 fetch1 stage 分支預測的 PC address 重新 fetch 指令
<ul>
<li>因此會產生 1 個 cycle 的 bubble，也就是 line/way 預測的 mis-penalty</li>
</ul>
</li>
<li>Alpha 21264 是使用 <a href="../superscalar-overview-ch3/#343---%E5%B0%87-tlb-%E5%92%8C-cache-%E6%94%BE%E5%85%A5%E6%B5%81%E6%B0%B4%E7%B7%9A">VIVT 架構</a>的 I-Cache，因此 PC address 可以直接比對</li>
<li>如果是使用 <a href="../superscalar-overview-ch3/#343---%E5%B0%87-tlb-%E5%92%8C-cache-%E6%94%BE%E5%85%A5%E6%B5%81%E6%B0%B4%E7%B7%9A">VIPT 架構</a>的 I-Cache，由於 tag 是 physical address，因此還需要將 fetch1 分支預測的 PC address (VA) 透過 TLB 轉換成 PA 才可以比對 line/way 的預測結果是否正確
<ul>
<li>因此，使用 line/way 預測時，使用 VIVT 架構的 I-Cache 是比較合理的設計</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Line/way 預測總結：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%204.png"></p>
</li>
</ul>
<h2 id="1122---分支預測">11.2.2 - 分支預測</h2>
<ul>
<li>
<p>為了獲得準確的分支預測，Alpha 21264 使用的分支預測器是比較複雜的，因此無法在 fetch0 stage 完成，需要使用 2 個 stages，在 fetch1 stage 才可以獲得預測結果</p>
<ul>
<li>這個結果會和 line/way 的預測結果做比較，如果發現不一致，就以分支預測的結果為主</li>
</ul>
</li>
<li>
<p>Alpha 21264 使用了競爭的分支預測法 (tournament branch prediction)；有些分支指令使用基於全局歷史的預測方法準確度比較高，有些分支指令則是使用基於局部歷史的預測方法準確度比較高，Alpha 21264 則是兩種預測方法都實現了；對於每條分支指令來說，處理器會根據兩種分支預測方法的準確度，動態地替每條分支指令選擇適合的分支預測方法，相當於這兩種分支預測方法彼此在競爭</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%205.png"></p>
<ul>
<li>
<p>左邊為基於局部歷史的分支預測器：</p>
<ul>
<li>Local history table ⇒ <strong>BHT</strong>：
<ul>
<li>大小為 1024 x 10 bits，也就是使用了 10 bits 的 BHR，可以記錄一條分支指令過去 10 次的分支結果，總共可以記錄 1024 條分支指令的歷史記錄</li>
</ul>
</li>
<li>Local prediction ⇒ <strong>PHT</strong>：
<ul>
<li>Alpha 21264 的 local history PHT 共包含了：2^10 = 1024 個 <code>saturating counter</code>，每個 saturating counter 為 3 bits (i.e. <code>3 bits saturating counter)</code>
<ul>
<li>共需：1024 x 3 bits</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>右邊為基於全局歷史的分支預測：</p>
<ul>
<li>Path history ⇒ <strong>GHR</strong>
<ul>
<li>GHR 為 12 bits，可以記錄過去 12 條分支指令的分支結果</li>
</ul>
</li>
<li>global prediction ⇒ <strong>PHT</strong>：
<ul>
<li>Alpha 21264 的 global history PHT 共包含了：2^12 = 4096 個 <code>saturating counter</code>，每個 saturating counter 為 2 bits (i.e. <code>2 bits saturating counter)</code>
<ul>
<li>共需：4096 x 2 bits</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Alpha 21264 基於局部歷史的分支預測器，採用了 <strong>non-speculative</strong> 的更新方法</p>
<ul>
<li>只有當指令 retire 時，才會更新分支預測器的內容 (e.g. BHR、saturating counter… etc)</li>
</ul>
</li>
<li>
<p>Alpha 21264 基於全局歷史的分支預測器，採用了 <strong>speculative</strong> 的更新方法</p>
<ul>
<li>一旦有分支指令得到預測結果，就將這個預測結果更新到 GHR 中
<ul>
<li>i.e. GHR 的狀態有可能是不正確的</li>
</ul>
</li>
<li>為了在 mis-prediction 時恢復 GHR，因此採用了 <a href="../superscalar-overview-ch4-part1/#425---%E5%88%86%E6%94%AF%E9%A0%90%E6%B8%AC%E7%9A%84%E6%9B%B4%E6%96%B0">Checkpoint GHR</a>；在更新 GHR 前，會將目前的 GHR 內容複製到 Checkpoint GHR 中；當發生 mis-prediction 時，就可以透過這個 Checkpoint GHR 來恢復 GHR</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%206.png"></p>
</li>
</ul>
</li>
<li>
<p>對於 PHT 的 saturating counters 一般都是在分支指令得到確定的結果後才更新 (參考：<a href="../superscalar-overview-ch4-part1/#425---%E5%88%86%E6%94%AF%E9%A0%90%E6%B8%AC%E7%9A%84%E6%9B%B4%E6%96%B0">link</a>)</p>
</li>
</ul>
<h1 id="113---暫存器重命名">11.3 - 暫存器重<strong>命名</strong></h1>
<ul>
<li>
<p>Alpha 21264 <a href="../superscalar-overview-ch7/#723---%E4%BD%BF%E7%94%A8%E7%B5%B1%E4%B8%80%E7%9A%84-prf-%E4%BE%86%E5%AF%A6%E5%81%9A%E6%9A%AB%E5%AD%98%E5%99%A8%E9%87%8D%E5%91%BD%E5%90%8D">使用了統一的 PRF 來實做暫存器重命名</a>，整個處理器中只有一個 PRF，一個 register 在它整個生命週期只會存在一的地方，不會發生位置的變化</p>
<ul>
<li>Register renaming 消除了 WAW 和 WAR 相關性</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%207.png"></p>
</li>
<li>
<p>Alpha 指令集中有一個特殊的 <code>CMOV (Conditional-move)</code> 指令，格式為：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-0-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-1">1</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span><span style="color:#a6e22e">CMOV</span>  <span style="color:#66d9ef">Ra</span>, <span style="color:#66d9ef">Rb</span> <span style="color:#960050;background-color:#1e0010">?</span> <span style="color:#66d9ef">Rc</span>  <span style="color:#75715e"># (Ra == 0) ? Rc = Rb : Rc = Rc
</span></span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>這條指令在 register renaming stage，需要讀取三個 source registers：<code>Ra</code>, <code>Rb</code>, <code>Rc</code>
<ul>
<li>
<p>由於 <code>Rc</code> 同時也是 destination register，因此需要分配一個 physical register 來存舊的 <code>Rc</code> 值，並再替 destination register：<code>Rc</code>，分配另一個 physical register</p>
</li>
<li>
<p>然而，對於 Alpha 的其他指令，都只需要讀取兩個 source registers 即可，因此不值得特別為 <code>CMOV</code> 指令增加 RAT 的 read port</p>
</li>
<li>
<p><code>CMOV</code> 指令可以拆解為下列兩條指令：</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-1-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-1">1</a>
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f" id="hl-1-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-2">2</a>
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-asm" data-lang="asm"><span style="display:flex;"><span><span style="color:#a6e22e">CMOV1</span>  <span style="color:#66d9ef">Ra</span>, <span style="color:#66d9ef">oldRc</span> -<span style="color:#960050;background-color:#1e0010">&gt;</span> <span style="color:#66d9ef">newRc1</span>
</span></span><span style="display:flex;"><span><span style="color:#a6e22e">CMOV2</span>  <span style="color:#66d9ef">newRC1</span>, <span style="color:#66d9ef">Rb</span> -<span style="color:#960050;background-color:#1e0010">&gt;</span> <span style="color:#66d9ef">newRc2</span>
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>透過這樣的方式，可以讓 <code>CMOV</code> 指令的 register renaming 按照一般的指令來處理，只是會需要分配兩個 physical registers</li>
</ul>
</li>
</ul>
</li>
<li>Alpha 21264 是一個 4-way superscalar CPU，每個 cycle 可以對四條指令做 register renaming，但如果這四條指令中存在 <code>CMOV</code> 指令，則會導致一個 cycle 內要對五條指令 register renaming
<ul>
<li>
<p>Alpha 21264 採用了比較簡單的方法，限制在做 register renaming 時，如果碰到 <code>COMV</code> 指令，則只有 <code>CMOV1</code> 指令以及其之前的指令可以在該 cycle 做 register renaming，<code>CMOV2</code> 以及其之後的指令必須等到下個 cycle 才可以做 register renaming</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%208.png"></p>
</li>
<li>
<p>雖然這樣的作法會讓 CPU 的執行效率有所降低，但 <code>CMOV</code> 指令出現的頻率並不高，且這種方法很容易實現，所以是一種可接受的折衷方法</p>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h1 id="114---發射">11.4 - 發射</h1>
<ul>
<li>Alpha 21264 包含了兩個 Issue Queue：Integer Issue Queue 以及 Floating-point Issue Queue，分別用來儲存整數類型的指令和浮點數類型的指令
<ul>
<li>Integer Issue Queue 可以儲存 20 條指令，並被 4 個 Integer FU 共享，每個 cycle 最多可以從 Integer Issue Queue 中選出 4 條整數指令出來執行</li>
<li>Floating-point Issue Queue 可以儲存 15 條指令，並被 2 個 Floating-point FU 共享，每個 cycle 可以從 Floating-point Issue Queue 中選出 2 條浮點數指令出來執行</li>
</ul>
</li>
<li>Alpha 21264 採用了 cluster 架構，將整數執行部份分成了 Cluster0 和 Cluster1 兩個部份，每個 cluster 都有完整的 Integer PRF，因此在處理器中共有兩個一模一樣的 Integer PRF
<ul>
<li>
<p>每個 cluster 內部又被分成了兩個 subcluster，分別為：upper (U) 和 lower (L)</p>
<ul>
<li>整數部份的 4 個 FU分佈在這幾個 subclusers 中：Cluster0 的 U0、L0 以及 Cluster1 的 U1、L1</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%209.png"></p>
<ul>
<li>Alpha 21264 並沒有直接實現 4-of-20 的 select 電路，而是：
<ul>
<li>Cluster0 中的 U0 和 U1 共用同一個 select 電路：select0</li>
<li>Cluster1 中的 L0 和 L1 共用同一個 select 電路：select1</li>
</ul>
</li>
<li>這樣每個 select 電路只需要實做 2-of-20 的功能即可</li>
</ul>
</li>
</ul>
</li>
<li>Alpha 21264 採用了 <a href="../superscalar-overview-ch8-part1/#813---%E5%A3%93%E7%B8%AE-vs-%E9%9D%9E%E5%A3%93%E7%B8%AE">Compressing Issue Queue</a>，使 select 電路可以很容易地實現 oldest-first 的功能，不過會增加 Issue Queue 的複雜度，功耗由於每條指令被 issued 時，都會需要移動大量的指令 (通常 oldest-first 的指令都是在 Issue Queue 的底部)，因此不適合使用在行動裝置上</li>
<li>Alpha 21264 中，除了 load 指令外，其他類型指令執行所需的 cycles 數都是固定的；Alpha 21264 簡化了 load 指令的 wake-up 機制：當 load 指令發生 D-Cache miss 時，在它的 <a href="../superscalar-overview-ch8-part2/#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">SW (Speculative Window)</a> 中的所有指令都會從 pipeline 中被 flushed 掉，這些指令會重新在 Issue Queue 中等待被 wake up，然後參與仲裁；對於那些與 load 無關的指令很快就會再次被 select 電路選中，然而那些與 load 指令相關的指令則需要等待 D-Cache miss 被解決
<ul>
<li>這種方式雖然降低了一點性能，但比較容易實現，因為不用識別 SW 中哪些指令和 load 指令存在相關性 (參考：<a href="../superscalar-overview-ch8-part2/#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">link</a>)，是一種可以接受的折衷方法</li>
</ul>
</li>
<li>為了配合上述的 wake-up 機制，指令在被 select 電路選中後，不能馬上”離開” Issue Queue，而是需要確認這條指令沒有被發生 D-Cache miss 的 load 指令所影響後，才能離開 Issue Queue
<ul>
<li>因此，一條指令被 select 電路選中後，在 Issue Queue 中需要等待 2 個 cycles，才可以從 Issue Queue 中刪除該指令
<ul>
<li><em>Cycle n</em>：
<ul>
<li>指令被 select 電路選中</li>
</ul>
</li>
<li><em>Cycle n + 1</em>：
<ul>
<li>指令仍會留在 Issue Queue 中，但不會再向 select 電路發出 request 訊號了</li>
</ul>
</li>
<li><em>Cycle n + 2</em>：
<ul>
<li>如果這條指令所需的 operands 都可獲得 (不論是來自 bypassing network 或是 PRF)，這條指令就可以被正常執行，並從 Issue Queue 中刪除該指令 (代表指令離開 Issue Queue 了)</li>
<li>如果這條指令所需的 operands 來自於前面的 load 指令，且該 load 指令發生了 D-Cache miss，那麼這條指令就沒辦法繼續被執行，需要重新”放回” Issue Queue 中並等待 D-Cache miss 被解決
<ul>
<li>實際上這條指令並沒有真的離開 Issue Queue (i.e. 還是佔著 Issue Queue 的 entry)，只需修改對應的 status bits 即可</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>浮點數的 load/store 指令也是在整數的 cluster 中被執行的</li>
</ul>
<h1 id="115---執行單元">11.5 - 執行單元</h1>
<h2 id="1151---整數的執行單元">11.5.1 - 整數的執行單元</h2>
<ul>
<li>
<p>Alpha 21264 共有 6 個 FU (4 個 Integer FU，2 個 Floating-point FU)，每個 cycle 可以同時執行六條指令</p>
<ul>
<li>i.e. Alpha 21264 是一個 machine width = 4，issue width = 6 的 superscalar CPU</li>
</ul>
</li>
<li>
<p>Alpha 21264 採用了 <a href="../superscalar-overview-ch8-part1/#812---%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-data-capture-vs-%E9%9D%9E%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-non-data-capture">non-data-capturing</a> 的架構，在指令被 select 電路選中後，才去讀 PRF</p>
<ul>
<li>Integer PRF 因此需要支持四條指令的讀取，也就是需要 8 個 read ports (假設每條指令有兩個 source registers)，導致執行速度較慢</li>
<li>為了解決這個問題，Alpha 21264 採用了 cluster 架構，將整數執行部份分為了 cluster0 和 cluster1，每個 cluster 都使用了一個完整的 Integer PRF；每個 cluster 內部又被分成了兩個 subcluster：upper (U) 以及 lower (L)
<ul>
<li>因此，每個 Integer PRF 只需 4 個 read ports，簡化了 PRF 的設計，加快執行速度</li>
</ul>
</li>
</ul>
</li>
<li>
<p>如果兩條連續的指令是在同一個 cluster 內執行，那麼就可以 back-to-back 的執行；然而如果兩條連續的指令是在不同的 clusters，如果要將一條指令的計算結果透過 bypassing network 傳給另一個 cluster 的指令，就需要跨越 cluster，經過比較長的電路，因此需要獨立使用一個 stage，導致兩條連續的指令之間會間隔一個 cycle，沒辦法 back-to-back 的執行</p>
<ul>
<li>一個 cluster 中指令執行的計算結果需要間隔一個 cycle 才能寫入另一個 cluster 的 PRF 中</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2010.png"></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2011.png"></p>
<ul>
<li>由於 Alpha 21264 是 out-of-order CPU，因此硬體會盡可能地找到一條不相關的指令插入間隔中，就不會降低處理器的執行效率了</li>
</ul>
</li>
</ul>
<h2 id="1152---浮點數的執行單元">11.5.2 - 浮點數的執行單元</h2>
<ul>
<li>
<p>Alpha 21264 有 2 個 Floating-point FU，每個 cycle 可以執行兩條 floating-point 指令</p>
</li>
<li>
<p>為了盡量減少連線的延遲，每個 cluster 中的 FU 都與自己的 PRF 緊靠在一起，大量地縮短 cluster 內 bypassing network 的電路，保證同一個 cluser 內連續的指令可以 back-to-back 的執行</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2012.png"></p>
</li>
<li>
<p>Alpha 21264 同一個 cluster 內，不同指令的 latency：</p>
<table>
	<thead>
			<tr>
					<th>Instruction class</th>
					<th>Latency (cycles)</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Simple integer operations</td>
					<td>1</td>
			</tr>
			<tr>
					<td>Special instruction</td>
					<td>3</td>
			</tr>
			<tr>
					<td>Integer multiply</td>
					<td>7</td>
			</tr>
			<tr>
					<td>Integer load</td>
					<td>3 (假設 D-Cache hit)</td>
			</tr>
			<tr>
					<td>Floating-point add</td>
					<td>4</td>
			</tr>
			<tr>
					<td>Floating-point load</td>
					<td>4 (假設 D-Cache hit)</td>
			</tr>
			<tr>
					<td>Floating-point multiply</td>
					<td>4</td>
			</tr>
			<tr>
					<td>Floating-point divide</td>
					<td>12 (single-precision); 15 (double-precision)</td>
			</tr>
			<tr>
					<td>Floating-point square-root</td>
					<td>12 (single-precision); 30 (double-precision)</td>
			</tr>
	</tbody>
</table>
<ul>
<li>如果跨 cluster，則需要再增加一個 cycle</li>
</ul>
</li>
</ul>
<h1 id="116---記憶體的存取">11.6 - 記憶體的存取</h1>
<ul>
<li>Alpha 21264 的訪問記憶體元件採用了兩個特殊的設計：
<ul>
<li>能夠對 load 指令和 store 指令之間存在的 RAW 相關性進行預測，從而規劃某些 load 指令進入 pipeline 的時間，防止它們提前進入 pipeline 做白工，稱為：<code>Speculative disambiguation</code></li>
<li>能夠對 load 指令存取 D-Cache 時是否 hit 做預測，從而避免不必要的 wake up，稱為：<code>Load hit/miss Prediction</code></li>
<li>以上兩種方法本質上都是透過預測的方式來提昇處理器的執行效率，並仍然在現代處理器中被廣泛地使用</li>
</ul>
</li>
</ul>
<h2 id="1161---speculative-disambiguation">11.6.1 - Speculative Disambiguation</h2>
<ul>
<li>
<p>對於 load 指令和 store 指令來說，它們之間的相關性在 register renaming stage 是無法被解決的，只有到了 execute stage，將指令中的存取位址計算出來後，才可以判斷它們之間是否存在相關性，也就是：<code>Memory Disambiguation</code></p>
</li>
<li>
<p>Alpha 21264 採用了<a href="../superscalar-overview-ch9/#961---memory-disambiguation">完全 out-of-order</a> 的方式來執行 load 指令和 store 指令，為了最大限度地減少對 pipeline 的負面影響，Alpha 21264 還對 load 指令是否和其之前的 store 指令存在相關性進行了預測</p>
<ul>
<li>如果預測一條 load 指令和 pipeline 中還沒有 retire 的 store 指令之間不存在 RAW 相應，那麼這條 load 指令就不須等待 store 指令的存取位置被計算出來，可以直接 out-of-order 進入 execute stage 被執行，提高了執行性能</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2013.png"></p>
<ul>
<li>Alpha 21264 將對 D-Cache 的存取拆分成兩個不同的 stages (<code>D-Cache1</code>、<code>DCache2</code>)：
<ul>
<li>D-Cache1 stage：讀取 D-Cache，但這個 cycle 沒辦法得到結果</li>
<li>D-Cache2 stage：得到 data 和 tag，並比較 tag，以判斷是否 cache hit</li>
</ul>
</li>
</ul>
</li>
<li>
<p>將 D-Cache 的存取採取了 pipeline 的方式：</p>
<ul>
<li>缺點：
<ul>
<li>增加了 load 指令的執行 cycles 數</li>
<li>增加了 load 指令和與其存在相關性的指令所間隔的 cycles 數，產生更多的 bubbles</li>
</ul>
</li>
<li>優點：
<ul>
<li>可以提高 CPU 的 frequency</li>
<li>Out-of-order CPU 會盡可能地找到不相關的指令插入間隔中，因此不會對性能造成太大的負面影響</li>
</ul>
</li>
<li>因此，現代處理器基本上都使用 pipeline 的方式來存取 D-Cache</li>
</ul>
</li>
<li>
<p>在 D-Cache1 stage 中，由於 load 指令和先前的 store 指令的存取位址都已經被計算出來，因此可以檢查 load 指令是否與先前的 store 指令存在相關性，也就是：<code>disambiguation</code>，可以透過下列的硬體元件來完成：</p>
<ol>
<li><strong>Load/store Issue Queue：</strong>
<ul>
<li>以 out-of-order 的方式將 load 指令和 store 指令送到 FU 來執行</li>
</ul>
</li>
<li><strong>Load Queue：</strong>
<ul>
<li>依照 program order 的順序儲存著所有 load 指令的存取位址</li>
<li>Load 指令在 register renaming stage 就會被寫進 load queue 中 (register renaming stage 還是 in-order 的)</li>
<li>當 load 指令 retire 時，就會將 load 指令在 load queue 中對應的 entry 給釋放</li>
<li>使用 CAM 來實現，以便可以快速的比較 load 指令的存取位址</li>
</ul>
</li>
<li><strong>Store Queue：</strong>
<ul>
<li>依照 program order 的順序儲存著所有 store 指令的存取位址</li>
<li>Store 指令在 register renaming stage 就會被寫進 store queue 中 (register renaming stage 還是 in-order 的)</li>
<li>當 store 指令 retire 時，就會將 store 指令在 store queue 中對應的 entry 給釋放</li>
<li>使用 CAM 來實現，以便可以快速的比較 store 指令的存取位址</li>
</ul>
</li>
<li><strong>Wait Table：</strong>
<ul>
<li>
<p>一個 1024 x 1 bit 的表格，<strong>使用 PC address 作為索引</strong>，用來儲存 load 指令的相關性資訊</p>
</li>
<li>
<p>Wait table 中的每個 bit 都稱為：<code>Wait bit</code>，初始值皆為 <strong>0</strong>，表示所有的 load 指令和其之前的 store 指令之間都不存在相關性，可以 out-of-order execution</p>
</li>
<li>
<p>當發現了 store/load 違例，也就是發現了一條或多條在 load 指令前的 store 指令，與該 load 指令所存取的記憶體地址相同 (實際上，只要是存取範圍有重疊就存在相關性)，此時就會將該 load 指令在 wait table 中所對應的 wait bit 給設成 <strong>1</strong></p>
</li>
<li>
<p>在 decode stage，每當 decode 出一條 load 指令，就需讀取 wait table，並將 load 指令所對應的 wait bit 一起寫進 Issue Queue 中</p>
<ul>
<li>如果一條 load 指令的 wait bit 為 1，則該 load 指令必須等到所有在其之前的 store 指令都計算出存取位址後，才可以向 select 電路發出 request 請求仲裁</li>
</ul>
</li>
<li>
<p>Wait table 每 16,384 個 cycles 就會全部清為 0，以避免 wait table 的內容在經過一段時間的執行後全部變成 1 (這樣所有的 load 指令都無法 out-of-order execute 了)</p>
<ul>
<li>就算是同一條 load 指令 (i.e. PC address 相同)，也不一定每次執行時都與 store 指令仍然存在相關性</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2014.png"></p>
</li>
</ul>
</li>
</ol>
</li>
<li>
<p>指令進到 issue stage 後的下一個 cycle，進到 register read stage 並從 PRF 中讀取 operands，然後再到下一個 cycle，進到 address calculation stage 就可以計算指令的存取位址了，將存取位址計算出來後，就可以進到 D-Cache1 stage</p>
</li>
<li>
<p>在 D-Cache1 stage 會檢查 load 指令和 store 指令之間是否存在相關性，因此這個 cycle stage 也被稱為 <code>Disambiguation stage</code>，load 指令和 store 指令的 disambiguation 分別為：</p>
<ol>
<li><strong>Load Disambiguation：</strong>
<ul>
<li>Load 指令會將其存取位址寫到 <strong>load queue</strong> 對應 entry 中，同時 load 指令還會完成以下兩件事情：
<ol>
<li>Load 指令會去查詢 <strong>store queue</strong>，如果發現存在同樣存取位址且年齡比該 load 指令還<strong>老</strong>的 <strong>store 指令</strong>，那這條 load 指令就不需要存取 D-Cache 了，直接透過 store queue 就可以得到結果</li>
<li>Load 指令還需要去查詢 <strong>load queue</strong>，Alpha 21264 要求訪問相同位址的 load 指令必須是 in-order (i.e. program-order) 的，如果不滿足這個規則，在 multi-core 的環境下有可能會發生錯誤
<ul>
<li>
<p>如果發現存在存取位址相同且比該 load 指令還要<strong>年輕</strong>的 <strong>load 指令</strong>，就代表發生了 load/load 指令違例：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2015.png"></p>
<ul>
<li>Load 指令 X 和 load 指令 Y 執行的中間有可能被其他的 CPU 將該存取位址的值給更動了，因此如果 load 指令的執行順序改變，其結果就會出錯</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
<li>Alpha 21264 定義了各種記憶體存取應該保持的執行順序：
<table>
	<thead>
			<tr>
					<th>First Instruction In Pair</th>
					<th>Second Instruction in Pair</th>
					<th>Reference Order</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td>Load memory to address X</td>
					<td>Load memory to address X</td>
					<td>Maintained</td>
			</tr>
			<tr>
					<td>Load memory to address X</td>
					<td>Load memory to address Y</td>
					<td>Not Maintained</td>
			</tr>
			<tr>
					<td>Store memory to address X</td>
					<td>Store memory to address X</td>
					<td>Maintained</td>
			</tr>
			<tr>
					<td>Store memory to address X</td>
					<td>Store memory to address Y</td>
					<td>Maintained</td>
			</tr>
			<tr>
					<td>Load memory to address X</td>
					<td>Store memory to address X</td>
					<td>Maintained</td>
			</tr>
			<tr>
					<td>Load memory to address X</td>
					<td>Store memory to address Y</td>
					<td>Not Maintained</td>
			</tr>
			<tr>
					<td>Store memory to address X</td>
					<td>Load memory to address X</td>
					<td>Maintained</td>
			</tr>
			<tr>
					<td>Store memory to address X</td>
					<td>Load memory to address Y</td>
					<td>Not Maintained</td>
			</tr>
	</tbody>
</table>
<ul>
<li>兩條 load 指令存取同一個位址，order 必須 maintain ⇒ 否則就是 load/load 指令違例</li>
<li>Store 指令一定是 in-order 執行，因此兩條 store 指令不論存取位址是否相同，order 都必須 maintain</li>
<li>一條比較年輕的 load 指令，與一條比較老的 store 存取同一個位址，order 必須 maintain ⇒ 否則就是 store/load 指令違例</li>
<li>一條比較年輕的 store 指令，與一條比較老的 load 指令存取同一個位址，order 必須 maintain ⇒ 否則就是 store/load 指令違例</li>
<li>其他狀況，都可以 out-of-order 執行</li>
<li>Alpha 21264 中 load 指令和 store 指令是 out-of-order 執行的，因此需要在 disambiguation stage 解決順序性</li>
<li>然而，load 指令在查詢 store queue 時，如果發現存在同樣存取位址且年齡比該 load 指令還老的 store 指令，這種情況不需要 flush pipeline，因為這只代表 load 指令可以直接從 store buffer 中讀取 store 指令的資料 (需等待 store 指令執行完畢)，不需存取 D-Cache，但是 store 指令和 load 指令之間的執行仍然要保持 in-order
<ul>
<li>
<p>但如果 load 指令需要的資料寬度大於 store 指令的資料寬度 (e.g. 32-bit load vs. 8-bit store) 時，代表 load 指令所需要的資料有一部分存在 store queue 中，另外一部份存在 D-Cache 中，load 指令就沒辦法在同一個 cycle 內得到其所需的資料了</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2016.png"></p>
<ul>
<li>此時，仍需將 load 指令和與其相關的指令都從 pipeline 中給 flush 掉，並重新從 I-Cache fetch 這些指令
<ul>
<li>在 Alpha 21264 中，這個過程稱為：<code>replay trap</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>Store Disambiguation：</strong>
<ul>
<li>
<p>在 pipeline 的 disambiguation stage，store 指令會將其計算出來的存取位址寫到 <strong>store queue</strong> 中對應的 entry 中，並在同一個 cycle 查詢 <strong>load queue</strong>，如果發現存在同樣存取位址且年齡比該 store 指令還年輕的 load <strong>指令</strong>，則代表這條提前執行的 load 指令沒有使用到正確的計算結果，產生了 store/load 指令違例</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2017.png"></p>
</li>
<li>
<p>當發生 store/load 指令違例時，最理想的解決方法是只將違例的 load 指令以及所有與其相關的指令從 pipeline 中給 flush 掉 (參考：<a href="../superscalar-overview-ch8-part2/#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">Selective Replay</a>)；然而這種設計增加了設計的複雜度，因此 Alpha 21264 採用了相對比較簡單的 replay trap 處理方法</p>
<ul>
<li>當已經離開 issue queue 的 load 指令或 store 指令由於某個原因不能再繼續執行時，這條 load 指令或 store 指令及它之後的所有指令都會從 pipeline 中被 flushed 掉，並恢復 CPU 的狀態 (Alpha 21264 有非常豐富的 checkpoints)，然後重新從 I-Cache fetch 這些指令來執行</li>
<li>Alpha 21264 中，load/load 指令違例也是採用 replay trap 的方式來處理</li>
</ul>
</li>
<li>
<p>缺點：</p>
<ul>
<li>降低了處理器的性能，如果經常發生 store/load 指令違例或是 load/load 指令違例，那麼就需要經常的 flush pipeline 中的部份指令，並重新從 I-Cache fetch 這些指令來執行
<ul>
<li>因此 Alpha 21264 才使用了 wait table 來對 store 和 load 之間的相關性進行預測，這樣可以避免大部分的 store/load 指令違例，降低需要 replay trap 的次數</li>
</ul>
</li>
</ul>
</li>
<li>
<p>當發生 store/load 指令違例後，除了進行 replay trap，還需要更新 wait table，將 load 指令在 wait table 所對應的 bit 給設成 1，這樣當下次這條違例的 load 指令再次被執行時，就需要等它之前所有的 store 指令都被 select 電路選中後，才允許這條 load 指令向 select 電路發出 request</p>
<ul>
<li>這條指令最後在執行時，有可能可以直接從 store queue 取得所需的資料</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2018.png"></p>
</li>
<li>
<p>Alpha 21264 選擇將這些被 flushed 的指令重新從 fetch stage 開始執行，而不是將違例的 load 指令及與其相關的所有指令重新放回 Issue Queue 中重新仲裁，是因為此時的 Issue Queue 可能已經沒有空間儲存這些指令了</p>
<ul>
<li>
<p>當 Issue Queue 沒有空間儲存這些指令時，這些指令就必須等待；這個等待時間可能會很長，而且需要額外一個硬體元件來儲存這些等待的指令</p>
</li>
<li>
<p>此外，由於這些指令沒辦法進入 Issue Queue，因此就不能 wake up Issue Queue 中其他的指令</p>
<ul>
<li>如果此時在 Issue Queue 中的指令正在等待這些指令來 wake up 它們，那麼在 Issue Queue 中的指令也沒辦法離開 Issue Queue，導致 Issue Queue 永遠沒有空間空出來，便會發生 <strong>dead lock</strong></li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2019.png"></p>
<ul>
<li>Store 指令在 disambiguation stage 發現在其之後的 load 指令先被執行了，因此發生了 store/load 指令違例</li>
<li>為了避免 dead lock，Alpha 21264 直接將第一組和第二組的全部指令從 pipeline 中給 flush 掉，並使用 checkpoint 來恢復 CPU 的狀態，然後重新從 I-Cache fetch 這些指令來執行</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Alpha 21264 採用的 replay 方法是基於 I-Cache 的，這種方法會降低處理器的效能，如果想基於 Issue Queue 來 replay，有兩種方式可以採用：</p>
<ol>
<li>
<p>當指令被 select 電路選中時，不離開 Issue Queue，只要等到指令 retire 時才允許其離開 Issue Queue (參考：<a href="../superscalar-overview-ch8-part2/#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">Issue Queue Base Replay</a>)</p>
<ul>
<li>
<p>優點：</p>
<ul>
<li>設計複雜度比較低</li>
</ul>
</li>
<li>
<p>缺點：</p>
<ul>
<li>由於指令被 select 電路選中後，並不會馬上離開 Issue Queue，只有在該條指令確認可以被正確執行後 (i.e. 不需要 replay)，才會允許該條指令離開 Issue Queue；然而實際上由於 D-Cache hit rate 是很高的，且發生 store/load 指令等違例的機率並不高，所以大部分指令被 select 電路選中後，其實並不需要 replay，但這些指令仍佔據著 Issue Queue 的空間，直到確認其不需要 replay 為止，導致 Issue Queue 中實際可用的 entries 數減少
<ul>
<li>增加 Issue Queue 的空間則會增加 select 和 wake-up 電路的 latency，因此無法無上限地增加</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>採用 <a href="../superscalar-overview-ch8-part2/#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">Replay Queue Based Replay</a> 的方式，增加一個 Replay Queue，用來保存離開 Issue Queue 但還沒 retire 的指令</p>
<ul>
<li>
<p>當發生 store/load 指令違例時，所有要被 replay 的指令只需從 Replay Queue 重新向 select 電路發出 request 即可</p>
<ul>
<li>Intel Pentium 4 採用了這種設計</li>
</ul>
</li>
<li>
<p>優點：</p>
<ul>
<li>提高了 Issue Queue 的使用效率</li>
</ul>
</li>
<li>
<p>缺點：</p>
<ul>
<li>Replay Queue 會佔用額外的硬體空間</li>
<li>Replay Queue 也需一起參與 select 電路的仲裁和 wake up，因此設計較複雜</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
</ol>
</li>
</ul>
<h2 id="1162---load-hitmiss-prediction">11.6.2 - Load hit/miss Prediction</h2>
<ul>
<li>
<p>在 Alpha 21264 中，load 指令執行的 cycles 數是不固定的，D-Cache hit/miss、D-Cache 中是否有 bank conflict、是否和其他的元件產生 D-Cache read port conflict 等，都會影響 load 指令執行的 cycles 數；但是，如果需要等到 load 指令讀取到資料後才 wake up Issue Queue 中等待的指令，會導致 latency 過長</p>
<ul>
<li>
<p>然而，大部分的情況下 D-Cache 都會是 hit 的，因此可以直接假設 D-Cache 總是 hit 來 wake up Issue Queue 中相關的指令，也就是採用 <a href="../superscalar-overview-ch8-part2/#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">Speculative wake-up</a></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2020.png"></p>
<ul>
<li>Load 指令被 select 電路選中後，需要等待 2 個 cycles 才可以 wake up 相關的指令</li>
<li>Cycel 4、cycle 5 和 load 相關的指令就可以被 wake up 了，這兩個 cycles 也稱為：<code>Speculative Window (SW)</code>
<ul>
<li>在這兩個 cycles 被 select 電路所選中的指令都不能離開 Issue Queue (i.e. 採用 <a href="../superscalar-overview-ch8-part2/#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">Issue Queue Based Replay</a> 的設計)，不論它們是否真的和 load 指令存在相關性</li>
<li>當發現 load 指令真的是 D-Cache hit 時，這兩條指令就可以離開 Issue Queue</li>
<li>但如果發現 load 指令是 D-Cache miss 時，這兩條指令就必須從 pipeline 中給 flush 掉，並在 Issue Queue 中重新等待被 wake up，然後重新向 select 電路發出 request</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Alpha 21264 中並沒有區分 SW 中哪些指令和 load 指令相關，而是假設全部指令都與 load 指令相關；在發現 D-Cache miss 時會將 SW 中的所有指令從 pipeline 中給 flush 掉，並在 Issue Queue 中重新等待被 wake up，此時會假設 L2 Cache 是 hit 的，因此這些指令可以再次 (在 cycle 6 時) 被 select 電路選中，這會導致 4 個 cycles 的 latency</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2021.png"></p>
<ul>
<li>如果 L2 Cache hit，那麼這些指令就可以從 Issue Queue 中離開</li>
<li>如果 L2 Cache miss，那麼這些指令就必須在 Issue Queue 中繼續等待</li>
</ul>
</li>
<li>
<p>在發現 D-Cache miss 時會需要將 SW 中的所有指令從 pipeline 中給 flush 掉，但這樣就浪費了執行效率，因為這些 cycles 原本可以選擇其他指令來執行</p>
<ul>
<li>為了盡量避免這種情況發生，Alpha 21264 中對 load 指令是否會 D-Cache hit 也進行了預測，只有那些預測 D-Cache hit 的 load 指令才會以 2 cycles latency 的方式來 wake up 相關的指令，否則就以 4 cycles latency (假設 L2 Cache 是 hit) 的方式來 wake up 相關的指令</li>
<li>Alpha 21264  使用了一個 <code>4-bit saturating counter</code> 來預測 load 指令是否會 D-Cache hit
<ul>
<li>每當 D-Cache hit 時，counter += 1</li>
<li>每當 D-Cache miss 時，counter -= 2</li>
<li>使用 counter 的 MSB 來作為 load 指令的預測值
<ul>
<li><code>MSB = 1</code>：預測 load 指令會 D-Cache hit</li>
<li><code>MSB = 0</code>：預測 load 指令會 D-Cache miss</li>
</ul>
</li>
</ul>
</li>
<li>在 <code>Independent Window (IW)</code> (i.e. 2 cycles latency 或是 4 cycles latency) 中可以選擇與 load 指令不相關的指令來執行，即使預測 D-Cache miss，這 4 個 cycles latency 由於仍然可以執行其他不相關的指令，因此也不會對 CPU 的性能造成太大的影響</li>
</ul>
</li>
<li>
<p>對於浮點數 load 指令來說，由於需要 128 bits 的資料，因此需要 2 個 cycles 才可以從 D-Cache 中讀出完整的結果 (64 bits x 2)，因此浮點數 load 指令的 latency 需要增加 1 個 cycle ⇒ 共 3 個 cycles</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2022.png"></p>
<ul>
<li>此時 SW 只有 1 個 cycle (從 load 指令等待 3 個 cycles 後開始將相關的指令 wake up，直到執行時發現是否為 D-Cache hit/miss，中間所間隔的 cycles 數)
<ul>
<li>如果發現 load 指令是 D-Cache hit 時，這條指令就可以離開 Issue Queue</li>
<li>如果發現 load 指令是 D-Cache miss 時，這條指令就必須從 pipeline 中給 flush 掉，並在 Issue Queue 中重新等待被 wake up，然後重新向 select 電路發出 request</li>
</ul>
</li>
</ul>
</li>
<li>
<p>當發生 D-Cache miss 時，需要從 L2 Cache 讀取資料，浮點數 load 指令 L2 Cache hit 的 latency 同樣需要增加 1 個 cycle ⇒ 共 5 個 cycles</p>
<ul>
<li>i.e. 指令 A 可以在 cycle 7 時再次被 select 電路選中</li>
</ul>
</li>
<li>
<p>對於浮點數 load 指令來說，由於其 SW 只有 1 個 cycle，且浮點數指令執行所需的 cycles 通常都比較長，當浮點數 load 指令發生 D-Cache miss 時有足夠的時間進行處理，因此浮點數 load 指令<strong>並不會</strong>使用 saturating counter 來預測其 D-Cache 是否會 hit</p>
</li>
</ul>
<h1 id="117---退休">11.7 - 退休</h1>
<ul>
<li>在 Alpha 21264 中，一條指令要 retire 時發現了 exception，那麼在 pipeline 中的所有指令都會被 flushed 掉，這些被 flushed 的指令所佔據的 physical registers 也會被釋放，並放回 free list 中，RAT 可以透過 checkpoint 來恢復到產生 exception 那道指令之前的狀態
<ul>
<li>Alpha 21264 的 ROB 中每條指令都有一個對應的 checkpoint，80 個 ROB entries 共對應了 80 個 checkpoints，以便快速的恢復 CPU 的狀態</li>
<li>Alpha 21264 的 RAT 是<a href="../superscalar-overview-ch7/#732---%E5%9F%BA%E6%96%BC-cam-%E7%9A%84%E9%87%8D%E5%91%BD%E5%90%8D%E6%98%A0%E5%B0%84%E8%A1%A8">使用 CAM 實現</a>的，因此佔用的硬體資源也是很小的</li>
</ul>
</li>
<li>Alpha 21264 一個 cycle 內最多可以 retire 8 條指令</li>
</ul>
<h1 id="118---結論">11.8 - 結論</h1>
<ul>
<li>
<p>Alpha 21264 為了達到很高的 CPU frequency，在很多地方都加了額外的 pipeline stage，例如 對 cache 的存取；然而，這也同時增加了 mis-prediction penalty，並增大了 load latency</p>
<ul>
<li>Out-of-order execution 可以緩解 load latency 增大所引起的問題</li>
</ul>
</li>
<li>
<p>Alpha 21264 使用了複雜的分支預測方法來準確地預測分支指令，為了加快 branch mis-prediction 及 exception 發生時恢復 CPU 狀態的效率，Alpha 21264 為每個 ROB 中的每條指令都分配了一個對應的 checkpoint</p>
</li>
<li>
<p>Alpha 21264 使用了 cluster 架構來解決 multi-port PRF 所產生的問題</p>
</li>
<li>
<p>Alpha 21264 使用 load hit/miss prediction 和 speculative disambiguation 這兩種預測方法來加快 load/store 指令的執行</p>
</li>
<li>
<p>更深的 pipeline 加上更準確的預測方法，使 Alpha 21264 在運行更快的 CPU frequency 的同時，保持了較高的執行效率</p>
</li>
<li>
<p>Alpha 21264 的 pipeline：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch11/image%2023.png"></p>
<ul>
<li>對於普通的指令，Alpha 21264 使用了 7 stages 的 pipeline</li>
<li>對於 load/store 類型的指令，由於對 D-Cache 的存取也採取了 pipeline 的方式，因此 Alpha 21264 使用了 9 stages 的 pipeline</li>
<li>對於 floating-point 類型的指令，Alpha 21264 使用了 10 stages 的 pipeline</li>
</ul>
</li>
<li>
<p>事實上，不能依 pipeline 的 stages 個數來判斷一個 CPU 的性能高低，而是還需要考慮 pipeline 的執行效率，而這需要使用更準確的各種預測方法，才能使 pipeline 保持高效率的執行</p>
</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>&lt;超標量處理器概覽&gt; 第 8 章 - 發射 (Part 2)</title>
      <link>https://0xc0de.xyz/posts/superscalar-overview-ch8-part2/</link>
      <pubDate>Sat, 19 Apr 2025 00:00:00 +0800</pubDate>
      <guid>https://0xc0de.xyz/posts/superscalar-overview-ch8-part2/</guid>
      <description>&lt;h1 id=&#34;85---喚醒-wake-up&#34;&gt;8.5 - 喚醒 (Wake up)&lt;/h1&gt;
&lt;h2 id=&#34;851---單週期指令的喚醒&#34;&gt;8.5.1 - 單週期指令的喚醒&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Wake up 是指被 select 電路選中的指令將其執行完後，將其 destination register 和 Issue Queue 中所有的 source registers 編號做比較，並將相同編號的 source registers 標記為 ready 的過程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;因為需要將被選中指令的 destination register 編號需要傳遞到 Issue Queue 中的所有 entries，當 Issue Queue 容量比較大，或是 Issue Queue 的個數比較多時，這個編號就需要走很長的路徑&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;一般情況下，select 電路的個數等同於 &lt;code&gt;issue width&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;以下是 &lt;em&gt;Issue width = 4&lt;/em&gt; 的 wake-up 電路 (只包含一個 Issue Queue 的 entry，且所有 select 電路共用同一個 Issue Queue)：&lt;/p&gt;
&lt;p&gt;&lt;img alt=&#34;image.png&#34; loading=&#34;lazy&#34; src=&#34;https://0xc0de.xyz/posts/superscalar-overview-ch8-part2/image.png&#34;&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SrcL&lt;/code&gt; /&lt;code&gt;SrcR&lt;/code&gt;：第一、第二個 source register 的編號&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ValL&lt;/code&gt; / &lt;code&gt;ValR&lt;/code&gt;：指令是否存在第一、第二個 source register&lt;/li&gt;
&lt;li&gt;&lt;code&gt;RdyL&lt;/code&gt; / &lt;code&gt;RdyR&lt;/code&gt;：第一、第二個 source register 是否準備好&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dest&lt;/code&gt;：Destination register 的編號&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Issued&lt;/code&gt;：指令是否已經被 issued 出去
&lt;ul&gt;
&lt;li&gt;當指令準備好，並被 select 電路選中時，此時該指令不一定會馬上被 issued 出去 (e.g. 一條指令使用了 load 指令的結果，即使其被 select 電路選中，也不可以馬上離開 Issue Queue)，因此需要透過 &lt;code&gt;Issued&lt;/code&gt; 記錄該指令是否已經被 issued 了
&lt;ul&gt;
&lt;li&gt;如果已經被 issued，select 電路就不會重複選到該指令&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;當指令準備好時，就會向 select 電路送 request 訊號，請求被仲裁&lt;/li&gt;
&lt;li&gt;Select 電路會選擇根據是否選擇了該指令，送出 grant 訊號
&lt;ul&gt;
&lt;li&gt;四個 select 電路同時間只會有一個 grant 訊號是 valid 的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果一條指令被 granted，那麼就會將其 &lt;code&gt;Dest&lt;/code&gt; 送到 tag bus 上，以便 broadcast 給所有的 Issue Queue entries&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;實務上，為了提供速度，需要減少 Issue Queue 的容量，因此可以使用多個容量較小的 Issue Queue&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="85---喚醒-wake-up">8.5 - 喚醒 (Wake up)</h1>
<h2 id="851---單週期指令的喚醒">8.5.1 - 單週期指令的喚醒</h2>
<ul>
<li>
<p>Wake up 是指被 select 電路選中的指令將其執行完後，將其 destination register 和 Issue Queue 中所有的 source registers 編號做比較，並將相同編號的 source registers 標記為 ready 的過程</p>
<ul>
<li>因為需要將被選中指令的 destination register 編號需要傳遞到 Issue Queue 中的所有 entries，當 Issue Queue 容量比較大，或是 Issue Queue 的個數比較多時，這個編號就需要走很長的路徑</li>
</ul>
</li>
<li>
<p>一般情況下，select 電路的個數等同於 <code>issue width</code></p>
</li>
<li>
<p>以下是 <em>Issue width = 4</em> 的 wake-up 電路 (只包含一個 Issue Queue 的 entry，且所有 select 電路共用同一個 Issue Queue)：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image.png"></p>
<ul>
<li><code>SrcL</code> /<code>SrcR</code>：第一、第二個 source register 的編號</li>
<li><code>ValL</code> / <code>ValR</code>：指令是否存在第一、第二個 source register</li>
<li><code>RdyL</code> / <code>RdyR</code>：第一、第二個 source register 是否準備好</li>
<li><code>Dest</code>：Destination register 的編號</li>
<li><code>Issued</code>：指令是否已經被 issued 出去
<ul>
<li>當指令準備好，並被 select 電路選中時，此時該指令不一定會馬上被 issued 出去 (e.g. 一條指令使用了 load 指令的結果，即使其被 select 電路選中，也不可以馬上離開 Issue Queue)，因此需要透過 <code>Issued</code> 記錄該指令是否已經被 issued 了
<ul>
<li>如果已經被 issued，select 電路就不會重複選到該指令</li>
</ul>
</li>
</ul>
</li>
<li>當指令準備好時，就會向 select 電路送 request 訊號，請求被仲裁</li>
<li>Select 電路會選擇根據是否選擇了該指令，送出 grant 訊號
<ul>
<li>四個 select 電路同時間只會有一個 grant 訊號是 valid 的</li>
</ul>
</li>
<li>如果一條指令被 granted，那麼就會將其 <code>Dest</code> 送到 tag bus 上，以便 broadcast 給所有的 Issue Queue entries</li>
</ul>
</li>
<li>
<p>實務上，為了提供速度，需要減少 Issue Queue 的容量，因此可以使用多個容量較小的 Issue Queue</p>
<ul>
<li>例如 FU 可以拆分成：
<ul>
<li>兩個 ALU、一個 Mul/Div、一個 Load/Store ⇒ 共四個 ALU</li>
<li>可以使兩個 ALU 共用同一個 Issue Queue，其他 FU 各自有其獨立的 Issue Queue</li>
</ul>
</li>
<li>雖然每個 FU 仍然需要使用一個 select 電路，且沒辦法減少 wake-up 電路的複雜度，但由於 Issue Queue 的容量變小了，select 電路的 latency 也會因此減少，加快 select 電路的速度，進而減少 select 和 wake-up 兩個 stages 對 CPU cycle time 的影響</li>
</ul>
</li>
<li>
<p>大部分的 RISC ALU 指令都是 single-cycle 完成的，這種指令可以在 select 電路選中指令時，同個 cycle wake up 其他 Issue Queue 中的指令，被 wake up 的指令在 execute stage 可以透過 bypassing network 獲得被選中指令的執行結果，進而實現 back-to-back 的執行</p>
</li>
<li>
<p><strong>Single-cycle 指令</strong>的 wake-up 流程：</p>
<ol>
<li>被 select 電路選中的指令，其 destination register 會被送到 tag bus 上</li>
<li>每一條 tag bus 上的值會跟所有 Issue Queue 中所有指令的 source registers 編號做比較，如果相等，則將該 source registers 標記為 ready</li>
<li>當 Issue Queue 中的指令所有的 registers 都準備好了，並且還沒有被 select 電路選中過，其就可以向對應的 select 電路發出 request 訊號，要求被仲裁</li>
<li>如果 select 電路選擇了其他優先度更高的指令 (如年齡更老的指令)，則這條指令可以在下一個 cycle 持續向 select 電路發出 request 訊號，要求被仲裁</li>
<li>Issue Queue 中這條指令根據 grant 訊號，當 grant 訊號為 valid 時，將其 destination register 的編號送到對應的 tag bus 上，用來 wake up Issue Queue 中所有相關的 source register；同時這條指令就可以被 issued 至 FU 中執行了</li>
</ol>
</li>
</ul>
<h2 id="852---多週期指令的喚醒">8.5.2 - 多週期指令的喚醒</h2>
<ul>
<li>當一條指令無法在 1 個 cycle 內執行完成時，就不能在被 select 電路選中的那個 cycle，wake up Issue Queue 中的其他指令，而需要根據它在 FU 執行的 cycles 數，delay 這個 wake-up</li>
<li>Wake-up 過程主要分為兩個階段：
<ol>
<li>將被選中的指令的 destination register 送到 tag bus 上 broadcast</li>
<li>將 tag bus 上的值與所有 Issue Queue 中的 source registers 編號做比較</li>
</ol>
<ul>
<li>因此如果要 delay wake-up，就是需要 delay 這兩個階段</li>
</ul>
</li>
<li>總共有兩種 delay 方法：
<ul>
<li><code>Delay tag broadcast</code></li>
<li><code>Delay wake-up</code></li>
</ul>
</li>
<li><strong>Delay tag broadcast：</strong>
<ul>
<li>
<p>當發現被 select 電路選中的指令執行 cycles 數 &gt; 1，則在被選中的那個 cycle，並不將這條指令的 destination register 編號送到 tag bus 上，而是根據這條指令所需執行的 cycles 數：<code>N</code>，延遲 <code>N - 1</code> 個 cycles 後，才將 destination register 編號送到 tag bus 上</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%201.png"></p>
<ul>
<li>乘法指令的執行 cycles 數為 <strong>3</strong>，因此 delay：<strong>3 - 1 = 2</strong> 個 cycles 才將其 destination register 編號送到 tag bug 上</li>
<li>但當乘法和其他操作共用一條 tag bus 時，如：Mul/Div FU，乘法和除法會共用同一個 FU 以及同一條 tag bus，則有可能會出現：
<ul>
<li>
<p>當乘法指令的 destination register 編號 delay 了幾個 cycles 後送到 tag bus 上，其他的指令 (e.g. 除法指令)，同時間也要使用這條 tag bus，產生了衝突</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%202.png"></p>
<ul>
<li><code>MUL</code> 和 <code>ADD</code> 共用同一個 FU 以及同一條 tag bus，當 <code>MUL</code> 要將其 destination register 編號送到 tag bus 上時，<code>ADD</code> 指令也同時要將其 destination register 編號送到同一條 tag bus 上，產生了衝突</li>
<li>解決方法：
<ul>
<li>
<p>增加這個 FU 所對應的 tag bus 的數量</p>
<ul>
<li>缺點：
<ul>
<li>當處理器中有很多這種情況時，會導致需要大量的 bus，增加 wake-up 電路的複雜度，因此在實際設計中是無法使用的</li>
</ul>
</li>
</ul>
</li>
<li>
<p>使用一個表格記錄當前 FU 中執行的指令所需的 cycles 數，後續被 select 電路選中的指令，如果從這個表格中發現了衝突，那麼這條被選中的指令就不會馬上被送到 FU 中執行，而是在下個 cycle 繼續參與仲裁</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%203.png"></p>
<ul>
<li>指令 A 需要 3 個 cycles 來執行，因此指令 A 在被 select 電路選中後，需要 delay 2 個 cycles 才 wake up 其他的指令</li>
<li>指令 B 需要 2 個 cycles 來執行，因此指令 B 在被 select 電路選中後，需要 delay 1 個 cycle 才 wake up 其他的指令</li>
<li>透過表格可以知道，指令 B 和指令 A 在 <em>cycle 2</em> 會同時使用 tag bus，也就是存在衝突，因此指令 B 是不會被允許在 <em>cycle 2</em> 被選中的，會在下個 cycle 繼續參與仲裁</li>
<li>表格的查詢和 select 電路是同步運行的，因此指令 B 即使被 select 電路選中，也有可能被表格否決掉
<ul>
<li>優點：
<ul>
<li>沒有增加電路的延遲</li>
</ul>
</li>
<li>缺點：
<ul>
<li>指令 B 被否決的那個 cycle，指令 C 因為不會產生 tag bus 衝突，因此原本也是可以被 select 電路選中的，但因為指令 C 比指令 B 來得新，因此 select 電路選擇了指令 B，結果指令 B 被表格給否決，浪費了一個 cycle，造成性能的下降</li>
</ul>
</li>
</ul>
</li>
<li>因此，也可以採用先查詢表格，如果表格否決了該指令，則該指令就不會對 select 電路發出 request 訊號，把機會讓給其他的指令
<ul>
<li>缺點：
<ul>
<li>需要先查詢表格，再讓 select 電路仲裁指令，因此 cycle time 會增加</li>
</ul>
</li>
</ul>
</li>
<li>具體要選擇哪種設計，取決於實際設計中的折衷和權衡</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li><strong>Delay wake-up：</strong>
<ul>
<li>
<p>被 select 電路選中的指令同樣會在當個 cycle 就將其 destination register 的編號 broadcast 到 tag bug 上，並和 Issue Queue 中所有的指令的 source registers 做比較；但此時比較結果相同的 source registers 的 ready bit 並不會馬上被設為 valid，而是會依據被 select 電路所選中的指令執行所需的 cycles 數，delay 相對應的 cycles 後，才將 ready bit 設為 valid</p>
</li>
<li>
<p>優點：</p>
<ul>
<li>這種方法不需要增加這個 FU 所對應的 tag bus 的數量</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%204.png"></p>
<ul>
<li>指令 A 需要 4 個 cycles 來執行，因此指令 A 在被 select 電路選中後，在 Issue Queue 中所有的 <code>R3</code> source registers (e.g. 指令 B 的 <code>R3</code>) 都需要 delay 3 個 cycles 後，ready bit 才會被設為 valid</li>
</ul>
</li>
<li>
<p>其中一種實做 delay wake-up 控制電路的方法：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%205.png"></p>
<ul>
<li>採用 <strong>shift register</strong> 來實現 delay wake-up</li>
<li>每個指令執行所需的 cycles 數在 decode stage 就可以得知了，可以在指令進入到 Issue Queue 時，將該指令的 <code>DELAY</code> 值一併寫入 Issue Queue 中</li>
<li>當一條指令被 select 電路選中時，除了將其 destination register 的編號 broadcast 到 tag bus 上，同時也會將其 <code>DELAY</code> 值一起 broadcast 到 delay bus 上
<ul>
<li>一般來說，每個 FU 都會對應一個 delay bus</li>
</ul>
</li>
<li>Issue Queue entry 中每個欄位的內容：
<ul>
<li><code>Freed</code>：
<ul>
<li>這個 Issue Queue entry 是否為空閒的</li>
<li>當一條指令被寫入此 Issue Queue entry 時，該 Issue Queue entry 就不是空閒的 (<code>Freed = 0</code>)</li>
<li>當該指令被 select 電路選中，且確定沒有問題，可以離開 Issue Queue 時，這個 Issue Queue entry 就會變為空閒的狀態 (<code>Freed = 1</code>)
<ul>
<li>一條指令被 select 電路選中時，並不一定馬上就會離開 Issue Queue
<ul>
<li>因為有可能此時 source registers 都還沒準備好 (e.g. 如果等待 load 指令的結果)，但處理器為了加速執行效率，採用了 <a href="#853---%E6%8E%A8%E6%B8%AC%E5%96%9A%E9%86%92">speculative wake-up</a> 的方式，此時就算該指令被 select 電路選中，也有可能會在之後的時間重新再次參與仲裁</li>
</ul>
</li>
</ul>
</li>
<li>最簡單管理 Issue Queue entry 的方式，就是等一條指令 retire 時，才將該 Issue Queue entry 變為空閒的狀態
<ul>
<li>缺點：
<ul>
<li>會導致許多無效的指令佔著 Issue Queue entries，使 Issue Queue 中可用的 entries 數變少，降低了處理器的執行效率</li>
</ul>
</li>
<li>因此現實中的處理器都會採用更高級的方法</li>
</ul>
</li>
</ul>
</li>
<li><code>Issued</code>：
<ul>
<li>是否被 select 電路選中
<ul>
<li>0：沒被 select 電路選中，當 source registers 皆 ready 時，向 select 電路發出 request 訊號</li>
<li>1：被 select 電路選中，不再向 select 電路發出 request 訊號</li>
</ul>
</li>
<li>被 select 電路選中的指令，並不一定馬上就會離開 Issue Queue，因此需要將該指令進行標記，使其不再繼續向 select 電路發出 request 訊號</li>
</ul>
</li>
<li><code>SrcL</code>：
<ul>
<li>指令的第一個 source register 的編號，會與 tag bus 上所有的 destination registers 編號值做比較</li>
</ul>
</li>
<li><code>SrcL_M</code>：
<ul>
<li>當 tag bus 上的 destination registers 與 <code>SrcL</code> 比較結果相同時，<code>SrcL_M</code> 就會被設為 1 (i.e. match)</li>
<li>當指令收到 select 電路的 grant 訊號時，<code>SrcL_M</code> 會被設為 <code>0</code>
<ul>
<li>被 select 電路選中的指令，就會將其 destination register 的編號 broadcast 到 tag bus 上，同時也會將其 <code>DELAY</code> 值一起 broadcast 到 delay bus 上</li>
</ul>
</li>
<li><code>SrcL_M</code> 為 <code>SrcL_SHIFT</code> 的 enable 訊號，當 <code>SrcL_M</code> 為 <code>1</code> 時，<code>SrcL_SHIFT</code> 每個 cycle 都會 arithmetic right shift 一位 (i.e. MSB 為 1 時，right shift 也會補 1)</li>
</ul>
</li>
<li><code>SrcL_SHIFT</code>：
<ul>
<li>
<p>當 tag bus 上的 destination registers 與 <code>SrcL</code> 比較結果相同時，被 select 電路選中的指令的 <code>DELAY</code> 值就會被寫入 <code>SrcL_SHIFT</code> 中</p>
<ul>
<li><code>DELAY</code> 值會被 encoded，如被 select 電路選中的指令執行需要 3 個 cycles，且 <code>DEALY</code> bit width 為 8 bits，那麼 <code>DELAY</code> 值就會被 encoded 為 <code>2’b11111100</code> (i.e. <code>bit[7:2]</code> 皆被設成 <code>1</code>)</li>
</ul>
</li>
<li>
<p>當 <code>SrcL_M</code> 為 <code>1</code> 時，<code>SrcL_SHIFT</code> 每個 cycle 都會 arithmetic right shift 一位</p>
</li>
<li>
<p>當 <code>SrcL_SHIFT</code> 的 LSB 為 <code>1</code> 時 (i.e. <code>Rdy = 1</code>)，就代表該指令可以被 wake up 了</p>
<ul>
<li>透過這樣的方式，就可以實現 delay wake-up</li>
</ul>
</li>
<li>
<p>當指令收到 select 電路的 grant 訊號時，<code>SrcL_SHIFT</code> 會被清為 <code>0</code></p>
</li>
</ul>
</li>
<li><code>Rdy(_R)</code>：
<ul>
<li>指令的第一個 source register 是否已經準備完成，其值為 <code>SrcL_SHIFT</code> 的 LSB</li>
</ul>
</li>
<li><code>SrcR</code>、<code>SrcR_M</code>、<code>SrcR_SHIFT</code>、<code>Rdy(_L)</code> 代表指令的第二個 source register，其定義皆與第一個 source register 相同</li>
<li><code>SrcR_imm_valid</code>：
<ul>
<li>指令的第二個 source register 是否為 immediate value</li>
</ul>
</li>
<li><code>DELAY</code>：
<ul>
<li>記錄該指令執行時所需的 cycles 數，在 decode stage 就可以得知</li>
<li>當該指令被 select 電路選中時，除了將其 destination register 的編號 broadcast 到 tag bus 上，同時也會將此 <code>DELAY</code> 值一起 broadcast 到 delay bus 上</li>
<li><code>DELAY</code> 值會被 encoded，如該指令執行需要 3 個 cycles，且 <code>DEALY</code> bit width 為 8 bits，那麼 <code>DELAY</code> 值就會被 encoded 為 <code>2’b11111100</code> (i.e. <code>bit[7:2]</code> 皆被設成 <code>1</code>)</li>
<li><code>DELAY</code> 的 bit width 由 FU 所有支援的指令中，執行所需 cycles 數最長的那個指令來決定</li>
</ul>
</li>
<li><code>ROB_ID</code>：
<ul>
<li>這條指令在 ROB 中的 index，用來表示該指令的年齡，使 select 電路可以實現 oldest-first 仲裁</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="853---推測喚醒">8.5.3 - 推測喚醒</h2>
<ul>
<li>
<p>先前的 wake-up 機制都是建立在指令的執行 cycles 數都是可以預知的前提，才可以替每個指令指定 <code>DELAY</code> 值，但實際上，還有很多指令的執行 cycles 數是沒辦法提前預知的：</p>
<ul>
<li>Load 指令：
<ul>
<li>Load 指令的執行 cycles 數取決於 D-Cache 是否 hit，甚至是 L2 Cache、L3 Cache 和 DRAM 的時間
<ul>
<li>DRAM 由於自身的結構 (e.g. queuing delay)，從其讀取資料所需的時間並不是一個固定的值</li>
</ul>
</li>
</ul>
</li>
<li>在某些處理器中，定義了一些特殊的情況，如：
<ul>
<li>PowerPC 603 CPU，當被乘數的值比較小時，乘法的結果可以提前被計算出來，稱為 early out</li>
<li>Intel Core2 CPU 對於除法操作也存在 early out</li>
<li>因此這些指令的執行 cycles 數並不是一個固定的值</li>
</ul>
</li>
</ul>
</li>
<li>
<p>對於這些執行 cycles 數不固定的指令，一個最簡單的處理方式，就是等這些指令執行完時，才 wake up 其他相關的指令</p>
</li>
<li>
<p>例如，針對 load 指令：</p>
<ul>
<li>
<p>當 load 指令 A 執行完畢後，才 wake up 指令 B，假設 D-Cache hit，那麼指令 B 就需要 delay 5 個 cycles</p>
<ul>
<li>如果在這之間找不到足夠的指令來執行，就會導致處理器執行效率的下降</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%206.png"></p>
</li>
<li>
<p>由於 D-Cache 是否 hit 可以在資料被讀出前得知，因此可以將 pipeline 改為：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%207.png"></p>
<ul>
<li><code>Cal Addr</code>：計算並得到 load address</li>
<li><code>TLB/Tag</code>：得知 D-Cache 是否 hit/miss
<ul>
<li>在此 cycle 就提前 wake up 相關的指令，減少所需的 delay cycles 數，指令 B 只需 delay 3 個 cycles</li>
</ul>
</li>
<li><code>Data</code>：讀出資料</li>
</ul>
</li>
<li>
<p>D-Cache hit 的機率應該是很高的，那麼可以進一步的假設 D-Cache 總是 hit，將 pipeline 改為：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%208.png"></p>
<ul>
<li>
<p>在 Cal Addr 就 wake up 相關的指令，並在 Bypass stage 就可以夠過 bypassing network 得到 load 指令的結果，指令 B 只需 delay 2 個 cycles</p>
</li>
<li>
<p>然而，如果 D-Cache miss 了，那麼就沒辦法透過 bypassing network 得到 load 指令的結果，指令 B 也就無法被執行</p>
<ul>
<li>此外，指令 B 也無法就此停住等待結果，因為指令 B 已經佔用了 FU 了，會導致其他的指令無法被執行</li>
</ul>
</li>
<li>
<p>因此，最好的方法就是直接將指令 B 重新放回 Issue Queue 中，並重新參與仲裁，讓 FU 繼續執行其他的指令</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%209.png"></p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>讀取 L1 Cache、L2 Cache、甚至是 L3 Cache 所需要的 cycles 數是固定的 (假設皆為 hit 的情況)，所以可以使用上述 delay wake-up 的方法</p>
</li>
<li>
<p>然而，對於讀取時間不固定的情況，如讀取 DRAM，就只能等到從 DRAM 中讀回資料後，才能 wake up 相關的指令</p>
<ul>
<li>這種方式會需要比較多的 delay cycles，性能較差</li>
<li>但如果在這之間可以找到足夠的指令來執行，就可以降低性能上的損失</li>
</ul>
</li>
<li>
<p>針對像 load 指令這種執行所需 cycles 數不確定的指令，如果等到指令的結果被計算出來後，才 wake up 與其相關的指令，會降低處理器的性能，因此需要使用預測的方法來預測指令執行所需的 cycles 數，在指令得到結果之前，就 wake up 相關的指令，這種方法就稱為 <code>speculative wake-up</code>：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2010.png"></p>
</li>
<li>
<p>既然是預測，就有可能會預測錯誤，因此就需要恢復預測錯誤前的狀態</p>
<ul>
<li>對於 load 指令，由於 D-Cache 的 hit rate 是很高的，因此可以假設所有的 load 指令的 D-Cache 都是 hit 的；當 load 指令被 select 指令選中後，就可以依照其最短的執行 cycles 數，wake up 與其相關的指令</li>
<li>當 load 指令真的存取 D-Cache 時，如果此時發現了 D-Cache miss，就代表預測錯誤，需要恢復預測錯誤前的狀態：
<ul>
<li>被 load 指令 wake up 的 source registers 都必須重新被設為 not ready</li>
<li>對於已經離開 Issue Queue 的指令，還需要將其從 pipeline stage 中給 flush 掉，並重新放回 Issue Queue 中，並等待 load 指令重新 wake up 它們
<ul>
<li>此時 load 指令可以繼續預測 L2 Cache 是 hit 的，依照 L2 Cache hit 時所需的執行 cycle 數，依照該 cycles 數 wake up 其相關的指令</li>
<li>如果此時預測也失敗，那麼就需要依照同樣的方式，恢復預測錯誤前的狀態</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Load 指令被 select 電路選中後，需要等待 2 個 cycles 才可以 wake up 相關的指令，在這 2 個 cycles，select 電路可以選擇和 load 指令不相關的指令，這 2 個 cycles 稱為 <code>Independent Window (IW)</code>；此外，從 load 指令等待 2 個 cycles 後開始將相關的指令 wake up，直到執行時發現是否為 D-Cache hit/miss，中間共間隔了 2 個 cycles，這 2 個 cycles 稱為 <code>Speculative Window (SW)</code>：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2011.png"></p>
<ul>
<li>SW 中的指令不一定和 load 指令存在相關性
<ul>
<li>如果被 load 指令 wake up 的指令並沒有被 select 電路選中，SW 中的指令就不會使用 load 指令的結果</li>
</ul>
</li>
<li>IW 中的指令一定和對應的 load 指令不存在相關性，但仍有可能與其他的 load 指令存在相關性
<ul>
<li>
<p>因為兩道 load 指令，其 IW 和 SW 有可能是重疊的：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2012.png"></p>
<ul>
<li>Load 3 指令的 IW 和 Load 1 指令的 SW 重疊</li>
<li>假設在 TLB/Tag stage 就可以得知 D-Cache 是 hit 或 miss，那麼在 <em>cycle 5</em> 的時候，如果 Load 1 指令發生了 D-Cache miss，那麼就需要將與其相關的指令 (i.e. 第四和第五條指令) 從 pipeline 中給 flush 掉，並重新放回 Issue Queue 中，等待重新被 wake up ⇒ 這個過程稱為 <code>replay</code>
<ul>
<li><strong>因為採用 speculative wake-up，預測失敗時，與其相關的指令是沒辦法獲得該指令的執行結果的，因此需要將與其相關的指令 replay</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>一條指令被 select 電路選中後，可以選擇：
<ol>
<li>直接從 Issue Queue 離開</li>
<li>繼續停留在 Issue Queue 中，直到被允許時才離開</li>
</ol>
<ul>
<li>這兩種設計方式會需要使用不同狀態的恢復方法，產生不同的執行效率和硬體消耗</li>
<li>常用的兩種方法：
<ul>
<li><code>Issue Queue Based Replay</code></li>
<li><code>Replay Queue Based Replay</code></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Issue Queue Based Replay：</strong></p>
<ul>
<li>
<p>採用這種設計，指令被 select 電路選中後，並不會馬上離開 Issue Queue，只有在該條指令確認可以被正確執行後 (i.e. 不需要 replay)，才會允許該條指令離開 Issue Queue</p>
<ul>
<li><span id="issued-flag"></span>
需要在 Issue Queue 中新增一個 <a href="#issued-flag"><code>issued</code></a> flag，標記該指令已經被 select 電路選中，但還沒離開 Issue Queue
<ul>
<li>當 <code>issued</code> flag 為 <code>1</code> 時，就不會繼續向 select 電路發出 request 訊號請求仲裁</li>
</ul>
</li>
<li>當這條指令需要 replay 時，會將 <code>issued</code> flag 清為 <code>0</code>，這樣它就可以繼續向 select 電路發出 request 訊號請求仲裁了</li>
</ul>
</li>
<li>
<p>實際上，有很多情況都會需要 replay 指令：</p>
<ul>
<li>
<p>Load 指令發生 D-Cache miss</p>
</li>
<li>
<p>採用 interleaving 架構的 D-Cache 發生了 bank 衝突</p>
</li>
<li>
<p>Store/load 指令的違例</p>
<ul>
<li>存取<strong>相同位址</strong>的 store、load 指令，結果 load 指令在 store 指令之前先被執行了
<ul>
<li>i.e. load 指令讀到了 store 指令前的結果</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Load/load 指令的違例：</p>
<ul>
<li>存取<strong>相同位址</strong>的兩條 load 指令，結果後面的 load 指令比前面的 load 指令先被執行了
<ul>
<li>在某些 multi-core 的處理器中，需要保持原始的程式執行順序</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>上述的情況都必須將與其相關的指令 replay，此時最簡單的方法就是讓所有的指令被 select 電路選中時，都不要馬上離開 Issue Queue，只有在確定指令可以被正確執行時，才將該指令從 Issue Queue 中移除</p>
</li>
<li>
<p>什麼時候才能確定指令可以被正確執行呢?</p>
<ul>
<li>如果是完全 out-of-order 來執行 load 和 store 指令：
<ul>
<li>即使 load 指令是 D-Cache hit，甚至 load 指令已經執行完畢，但只要 load 指令還沒有 retire，都有可能會發生：
<ul>
<li><strong>store/load 指令違例：</strong>
<ul>
<li>load → store 同一個位址，結果 store 指令比 load 指令先被執行了，而導致 load 指令的結果是錯誤的</li>
</ul>
</li>
<li><strong>load/load 指令違例：</strong>
<ul>
<li>load → load 同一個位址，結果比較年輕的 load 指令比比較老的 load 指令先被執行了
<ul>
<li>在 multi-core 的環境下，有可能兩條 load 指令之間會有另一個 CPU 的 store 指令寫入了同一個位址，因此兩條 load 指令的執行順序應該要保持 in-order</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>此時需要將 load 指令和與其相關的指令都 replay (同時把 <code>issued</code> flags 重新設回 <code>0</code>)
<ul>
<li>P.S. store → load 同一個位址，結果 load 指令比 store 指令先被執行了，這種情況<strong>並不算是指令違例，因此不需要 replay</strong>，因為這只代表 load 指令可以直接從 store buffer 中讀取 store 指令的資料 (需等待 store 指令執行完畢)，不需存取 D-Cache，但是 store 指令和 load 指令之間的執行仍然要保持 in-order
<ul>
<li>但如果 load 指令需要的資料寬度大於 store 指令的資料寬度 (e.g. 32-bit load vs. 8-bit store) 時，代表 load 指令所需要的資料有一部分存在 store buffer 中，另外一部份存在 D-Cache 中，load 指令就沒辦法在同一個 cycle 內得到其所需的資料了
<ul>
<li>此時，仍需將 load 指令和與其相關的指令都 replay (同時把 <code>issued</code> flags 重新設回 <code>0</code>)</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>因此，只有在一條指令 retire 後，才能保證其是正確執行的，此時該指令才可以離開 Issue Queue</li>
</ul>
</li>
<li>如果是完全 in-order 或是 partially out-of-order 來執行 load 和 store 指令，則不會出現上述的違例情況
<ul>
<li>此時只有 load 指令的 SW 中的指令才有可能需要 replay <strong>(load 指令本身不需要 replay)</strong></li>
<li>因此，只需等到 load 指令確定是 D-Cache hit/miss 的結果後，就可以決定 SW 中的指令是否可以離開 Issue Queue 了
<ul>
<li>D-Cache hit ⇒ SW 中的指令不需要 replay ⇒ load 指令和其 SW 中的指令可以離開 Issue Queue</li>
<li>D-Cache miss ⇒ SW 中的指令需要 replay ⇒ SW 中的指令無法離開 Issue Queue</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>優點：</p>
<ul>
<li>設計複雜度比較低</li>
</ul>
</li>
<li>
<p>缺點：</p>
<ul>
<li>由於指令被 select 電路選中後，並不會馬上離開 Issue Queue，只有在該條指令確認可以被正確執行後 (i.e. 不需要 replay)，才會允許該條指令離開 Issue Queue；然而實際上由於 D-Cache hit rate 是很高的，且發生 store/load 指令等違例的機率並不高，所以大部分指令被 select 電路選中後，其實並不需要 replay，但這些指令仍佔據著 Issue Queue 的空間，直到確認其不需要 replay 為止，導致 Issue Queue 中實際可用的 entries 數減少
<ul>
<li>增加 Issue Queue 的空間則會增加 select 和 wake-up 電路的 latency，因此無法無上限地增加</li>
</ul>
</li>
</ul>
</li>
<li>
<p>由於上述的缺點，針對<em>與 load 指令相關的指令</em>，可以採取適當的折衷：</p>
<ul>
<li>一旦 load 指令在執行階段得到 <strong>D-Cache hit</strong> 的結果，就直接釋放與其相關指令 (有被 select 電路選中的指令) 的 Issue Queue entries
<ul>
<li>優點：
<ul>
<li>這樣可以最大限度的減少指令對 Issue Queue 的佔用</li>
</ul>
</li>
<li>然而，這樣的折衷方式會導致當發生 store/load 指令違例，或是 load/load 指令違例時，其 replay 的方式會有所不同 (P.S. 只有採用完全 out-of-order 來執行 load 和 store 指令在 D-Cache hit 後還有可能會發生違例)：
<ul>
<li>由於很多需要 replay 的指令已經離開 Issue Queue 了，此時由於 Issue Queue 中有可能已經沒有多餘的空間了，因此就沒辦法將這些指令重新加回 Issue Queue 中，且有可能會發生 deadlock 的情況
<ul>
<li>需要被 replay 的指令因為 Issue Queue 中沒有空間，因此無法被 replay，導致那些依賴這些 replay 指令的指令，也無法被 wake up 來釋放 Issue Queue 的空間</li>
</ul>
</li>
<li>因此一旦需要 replay，就必須 flush 掉這些需要被 replay 的指令，並從 I-Cache 中重新讀取這些指令進 pipeline；這樣會損失一些性能，但這就是折衷的方法</li>
</ul>
</li>
</ul>
</li>
<li>但當 load 指令在執行階段得到 <strong>D-Cache miss</strong> 的結果時，一個比較簡單的方法就是將所有在 load 指令之後被 select 電路選到的指令都重新”放回” Issue Queue 中 (P.S. 只是將 <code>issued</code> flag 重新設回 <code>0</code>，因為這些指令還沒真的離開 Issue Queue)
<ul>
<li>
<p>然而，考量到 IW 的存在，甚至是 SW 中的指令，並不一定都與 load 指令存在相關性，這些原本並不需要 replay 的指令也會一併被 replay</p>
</li>
<li>
<p>這種方法稱為：<code>Non-Selective Replay</code></p>
</li>
<li>
<p>優點：</p>
<ul>
<li>實做比較簡單</li>
</ul>
</li>
<li>
<p>缺點：</p>
<ul>
<li>由於不用 replay 的指令也 replay 了，因此在一定程度上損失了一些性能</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2013.png"></p>
<ul>
<li>假設 <code>LD</code> 指令在 Data stage 得知 D-Cache miss，則需將 <code>LD</code> 指令之後被 select 電路選中的指令，全部從 pipeline 中 flush 掉，並重新”加回” Issue Queue 中等待重新被 wake up</li>
<li>但其實只有 <code>OR</code> 和 <code>SUB</code> 指令跟 <code>LD</code> 是有相關的，其他的指令其實沒有必要一起 replay</li>
</ul>
</li>
</ul>
</li>
<li>改進方法：
<ul>
<li>由於 IW 中的指令一定是與 load 指令無關的，因此處於 <code>Cal Addr</code> 和 <code>TLB/Tag</code> 這兩個 stages 的指令其實是可以不用 replay 的，只需將 <code>Select</code> 和 <code>RF</code> 這兩個 stages 的指令 replay 即可</li>
</ul>
</li>
</ul>
</li>
<li>
<p>如何判斷指令所操作的暫存器是否與 load 指令存在相關性?</p>
<ul>
<li>相關性分為兩種：
<ul>
<li>直接相關性
<ul>
<li>如上例的 <code>OR</code> 指令</li>
</ul>
</li>
<li>間接相關性
<ul>
<li>如上例的 <code>SUB</code> 指令
<ul>
<li><code>SUB</code> 指令與 <code>OR</code> 指令相關，<code>OR</code> 指令又與 <code>LD</code> 指令相關</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>可以使用一個 <code>load-vector</code> 來記錄 pipeline 中所有的 load 指令，每一個 bit 表示一條 load 指令，這個值的寬度等於 pipeline 中最多可以容納的 load 指令個數
<ul>
<li>
<p>每個 architecture register (或是經過 register renaming 後，physical register)，都會分配一個 load-vector</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2014.png"></p>
<ul>
<li>假設由左至右：bits[0] → bits[4]</li>
<li>第一條 <code>load</code> 指令佔據 load-vector 一個 bit：<code>R1</code> ⇒ <code>2’b10000</code></li>
<li>第二條 <code>ADD</code> 指令，因為依賴 <code>load</code> 指令的 <code>R1</code>，因此其 load-vector = 第一條 load 指令的 load vector：<code>R1</code> ⇒ <code>2’b10000</code></li>
<li>第三條 <code>load</code> 指令佔據 load-vector 額外一個 bit：<code>bits[1]</code>，且因其也依賴第一條 load 指令的 <code>R1</code>，因此其 load vector 的值等於兩者 OR 後的結果：<code>R3</code> ⇒ <code>2’b01000 | 2’b10000</code> = <code>2’b11000</code></li>
<li>第四條 <code>SUB</code> 指令依賴第三條 <code>load</code> 指令的 <code>R3</code>，因此其 load-vector = 第三條 <code>load</code> 指令的 load-vector：<code>R4</code> ⇒ <code>2’b11000</code></li>
<li>第五條 <code>load</code> 指令佔據 load-vector 額外一個 bit：<code>bits[2]</code>：<code>R5</code> ⇒ <code>2’b00100</code></li>
<li>第六條 <code>ADD</code> 指令依賴 <code>SUB</code> 的 <code>R4</code>，以及第五條 <code>load</code> 指令的 <code>R5</code>，因此其 load vector 的值等於兩者 load-vector 值 OR 後的結果：<code>R7</code> = <code>2’b11000 | 2’b00100</code> = <code>2’b11100</code></li>
</ul>
</li>
<li>
<p>每當 load 指令發生 D-Cache miss 時，就可以透過查詢 Issue Queue 中每個指令的 source registers 的 load-vector，是否有 bit 與 load 指令的 load-vector 重疊，進而得知是否與 load 指令存在相依性</p>
</li>
<li>
<p>缺點：</p>
<ul>
<li>如果 pipeline 深度比較深，可以容納的 load 指令數跟著增加，所需的 load-vector 的寬度也會需要跟著增加，增大硬體面積
<ul>
<li>Load-vector 只適合 pipeline 深度不深的 CPU</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>每條 load 指令可以改用 5 個 bits 的 <code>Load Position Value (LPV)</code> 來記錄 load 指令在 pipeline 中哪個 stage (<code>Select</code>、<code>RF</code>、<code>Cal Addr</code>、<code>TLB/Tag</code>、<code>Data</code>，假設 load 指令在 FU 中執行需要 3 個 cycles)
<ul>
<li>
<p>每當一條 load 被 select 電路選中時，都會將其 LPV 重置為 <code>2’b10000</code>，表示這條 load 指令目前處在 <code>Select</code> stage</p>
<ul>
<li>在 <code>Select</code> stage，load 指令的 destination register 編號都會與 Issue Queue 中所有指令的 source registers 編號做比較，如果相符，則每個相符的 source register 都會得到該 load 指令的 LPV</li>
<li>每條指令在被 select 電路選中並 wake up 其他指令時，如果被 wake up 的指令的 source registers 與被 select 電路選中指令的 destination register 相同，則被 select 電路選中指令的 destination register 的 LPV 值也會一併被複製給這些被 wake up 的指令的 source registers，以識別哪些指令的 source registers 與 load 指令存在間接相關性</li>
<li>之後的每個 cycle，所有 source registers 的 LPV 都會 right shift 一位，用來追蹤 load 指令在 pipeline 中的 stage</li>
<li>同時，load 指令的 LPV 每個 cycle 也會 right shift 一位，記錄其在 pipeline stage 中哪個 stage</li>
</ul>
</li>
<li>
<p>當 load 指令抵達 Data stage，發現 D-Cache miss 時，在 Issue Queue 中那些 LPV 的最低位 (<code>bits[0]</code>) 為 <code>1</code> 的 source registers，都與這條 load 指令存在直接或間接的相關性，需要 replay 並將這些 source registers 在 Issue Queue 中對應的 <code>Rdy</code> bit 設為 <code>0</code></p>
</li>
<li>
<p>範例：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2015.png"></p>
<ul>
<li><code>LD</code> 指令被 select，wake up <code>XOR</code> 指令，將 <code>LD</code> 的 LPV：<code>2’b10000</code> 複製給 <code>XOR</code> 指令的 source register：<code>R2</code></li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2016.png"></p>
<ul>
<li><code>XOR</code> 指令被 select，wake up <code>ADD</code> 和 <code>SLL</code> 指令，將 <code>XOR</code> 此時的 LPV：<code>2’b00010</code> 複製給 <code>ADD</code> 指令和 <code>SLL</code> 指令的 source register：<code>R3</code></li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2017.png"></p>
<ul>
<li><code>ADD</code> 指令和 <code>SLL</code> 指令被 select，<code>ADD</code> 指令 wake up <code>AND</code> 指令，並將其 LPV：<code>2’b00001</code> 複製給 <code>AND</code> 指令的 source register：<code>R4</code></li>
<li>此外，<code>AND</code> 指令還依賴 <code>LD2</code> 指令，因此也會將 <code>LD2</code> 指令的 LPV：<code>2’b00100</code> 複製給 <code>AND</code> 指令的 source register：<code>R6</code></li>
<li>此時 <code>LD1</code> 已經進到 <code>Data</code> stage 了 (假設在 <code>Data</code> stage 才可以得知 D-Cache hit 或是 miss 的即果)，所有與 <code>LD1</code> 指令有相關性的 source registers 其 LPV 最低位 (<code>bits[0])</code> 皆為 <code>1</code>
<ul>
<li>如果 D-Cache hit，那就繼續執行即可</li>
<li>如果 D-Cache miss，則必須：
<ul>
<li>將這些相關的 source register 在 Issue Queue entry 中的 <code>Rdy</code> bit 設為 <code>0</code></li>
<li>Replay SW 中與 load 指令相關的指令</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>優點：</p>
<ul>
<li>LPV 所需的 bits 數不會隨著 pipeline stages 的深度而改變，只會與 load 指令執行所需的 stages 個數有關</li>
<li>相較於 load-vector 的方法，硬體面積比較小，執行速度也比較快</li>
</ul>
</li>
<li>
<p>當一個 cycle 可以執行多條的 load 指令時，那麼在 Issue Queue 中每條指令 source register 的 LPV 電路個數就需要做相對應的複製</p>
<ul>
<li>例如：每個 cycle 可以執行 2 條 load 指令，那麼每個 source register 就需要 2 個 LPV 分別記錄這兩條 load 指令的 LPV</li>
<li>不過由於 D-Cache ports 數的限制，因此每個 cycle 可以執行的 load 指令個數並不會過多，因此此方法並不會對增加太多的硬體面積</li>
</ul>
</li>
<li>
<p>此外，LPV 也可以作為 select 電路選擇指令的依據：</p>
<ul>
<li>如果 LPV 為 <code>0</code>，代表該指令的 source register <strong>沒有與任何 load 指令相關</strong>，select 電路可以<strong>優先選擇該指令</strong></li>
<li>反之，如果 LPV 不為 <code>0</code>，代表該指令的 source register <strong>與某個 load 指令相關</strong>，select 電路可以<strong>優先選擇其他沒有相關性的指令</strong></li>
</ul>
</li>
<li>
<p>這種只 replay 與 load 指令相關的指令的方法，稱為：<code>Selective Replay</code></p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Non-Selective Replay 總結：</p>
<ul>
<li>Load 指令在執行階段發現 D-Cache miss 時，就將 SW 中<strong>所有</strong>指令都 replay，並將 <code>issued</code> flag 清為 <code>0</code>；此外，與 load 指令相關的 source registers 的 <code>Rdy</code> bit 也會被清為 <code>0</code>
<ul>
<li>由於與 load 指令不相關的指令其 source registers 的 <code>Rdy</code> bit 都仍為 <code>1</code>，因此在下一個 cycle 馬上就可以向 select 電路發出 request 訊號請求仲裁</li>
<li>與 load 指令相關的指令則要等之後再次被 wake up 後才可以重新參加仲裁</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Selective Replay 總結：</p>
<ul>
<li>
<p>透過 LVM，當 load 指令在執行階段發現 D-Cache miss 時，<strong>只將 SW 中與 load 相關</strong>的指令 replay</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2018.png"></p>
<ul>
<li>只需 replay 與 load 指令相關的 <code>OR</code> 和 <code>SUB</code> 指令</li>
</ul>
</li>
</ul>
</li>
<li>
<p>基於 Issue Queue replay 的方法，很多指令在被 select 電路選擇後沒辦法馬上離開 Issue Queue，因此其最大的缺點就是降低了 Issue Queue 中可以使用的空間，降低了處理器的執行效率</p>
</li>
</ul>
</li>
<li>
<p><strong>Replay Queue Based Replay：</strong></p>
<ul>
<li>
<p>採用這種設計，指令被 select 電路選中後，會馬上離開 Issue Queue，並存入 <strong>Replay Queue</strong> 中</p>
<ul>
<li>當一條指令 retire 時，就可以從 Replay Queue 中移除</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part2/image%2019.png"></p>
</li>
<li>
<p>舉例來說，當 load 指令發生 D-Cache miss 時：</p>
<ul>
<li>需將與 load 指令相關的指令，從 pipeline 中給 flush 掉</li>
<li>將這些相關的指令從 Replay Queue wake up，且這些指令被 select 電路仲裁的 priority，比其他在 Issue Queue 中的指令來得高，以便這些需要 replay 的指令可以優先重新送入 FU 中執行</li>
</ul>
</li>
<li>
<p>Replay Queue Based Replay 同樣支援處理：</p>
</li>
<li>
<p>優點：</p>
<ul>
<li>提高了 Issue Queue 的使用效率</li>
</ul>
</li>
<li>
<p>缺點：</p>
<ul>
<li>Replay Queue 會佔用額外的硬體空間</li>
<li>Replay Queue 也需一起參與 select 電路的仲裁和 wake up，因此設計較複雜</li>
</ul>
</li>
</ul>
</li>
<li>
<p>從 select stage 到 execute stage 所需的 cycles 數越多，需要 replay 的指令也就越多，因此：</p>
<ul>
<li>對於 <a href="../superscalar-overview-ch8-part1/#812---%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-data-capture-vs-%E9%9D%9E%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-non-data-capture">data-capture</a> 架構的設計：
<ul>
<li>由於其 select stage → execute stage 的時間比較短，需要 replay 的指令個數也就比較少
<ul>
<li>比較適合搭配 Issue Queue Based Replay 的設計</li>
</ul>
</li>
</ul>
</li>
<li>對於 <a href="../superscalar-overview-ch8-part1/#812---%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-data-capture-vs-%E9%9D%9E%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-non-data-capture">non-data-capture</a> 架構的設計：
<ul>
<li>由於指令在 issued 後會讀取 physical register file，所以其 select stage → execute stage 的時間比較長，需要 replay 的指令個數也就比較多
<ul>
<li>採用 Issue Queue Based Replay 的設計會導致 Issue Queue 的可用空間變少</li>
<li>比較適合搭配 Replay Queue Based Replay 的設計</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>&lt;超標量處理器概覽&gt; 第 8 章 - 發射 (Part 1)</title>
      <link>https://0xc0de.xyz/posts/superscalar-overview-ch8-part1/</link>
      <pubDate>Wed, 09 Apr 2025 00:00:00 +0800</pubDate>
      <guid>https://0xc0de.xyz/posts/superscalar-overview-ch8-part1/</guid>
      <description>&lt;h1 id=&#34;81---概述&#34;&gt;8.1 - 概述&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Issue 就是將符合一定條件的指令從 &lt;code&gt;Issue Queue&lt;/code&gt; 中選出來，並送到 FU (Function Unit) 中執行的過程&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Issue Queue 會依照一定的規則，選擇那些 source registers 都已經準備好的指令，將其送到 FU 中執行，這個過程就稱為：&lt;code&gt;Issue&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Issue Queue 也被稱為 &lt;code&gt;Reservation Station&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;對於 in-order core，指令會按照程式原始順序加進 Issue Queue 中，此時 Issue Queue 就相當於一個 FIFO&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;對於 out-of-order core，只有少數指令，如 store 及分支指令，才會採用 in-order 的方式執行，對其他大部分的指令，都是採用 out-of-order 的方式執行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;指令到了 Issue Queue 後，只要 Issue Queue 中任一條指令的 operands 都準備好了，且滿足 issue 的條件，就可以將其送到 FU 中執行&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Issue Queue 設計的好壞，會直接影響處理器的 concurrency&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Issue stage 的硬體比較複雜，一般它都在 critical path 上，直接影響 CPU 的 cycle time&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="81---概述">8.1 - 概述</h1>
<ul>
<li>
<p>Issue 就是將符合一定條件的指令從 <code>Issue Queue</code> 中選出來，並送到 FU (Function Unit) 中執行的過程</p>
<ul>
<li>Issue Queue 會依照一定的規則，選擇那些 source registers 都已經準備好的指令，將其送到 FU 中執行，這個過程就稱為：<code>Issue</code></li>
</ul>
</li>
<li>
<p>Issue Queue 也被稱為 <code>Reservation Station</code></p>
</li>
<li>
<p>對於 in-order core，指令會按照程式原始順序加進 Issue Queue 中，此時 Issue Queue 就相當於一個 FIFO</p>
</li>
<li>
<p>對於 out-of-order core，只有少數指令，如 store 及分支指令，才會採用 in-order 的方式執行，對其他大部分的指令，都是採用 out-of-order 的方式執行</p>
</li>
<li>
<p>指令到了 Issue Queue 後，只要 Issue Queue 中任一條指令的 operands 都準備好了，且滿足 issue 的條件，就可以將其送到 FU 中執行</p>
</li>
<li>
<p>Issue Queue 設計的好壞，會直接影響處理器的 concurrency</p>
</li>
<li>
<p>Issue stage 的硬體比較複雜，一般它都在 critical path 上，直接影響 CPU 的 cycle time</p>
<ul>
<li>因此這個 stage 的設計需要一定的折衷，才能在性能和複雜度之間找到一個可以接受的平衡點</li>
</ul>
</li>
<li>
<p>Issue stage 同時也是 in-order 和 out-of-order 的分界點</p>
<ul>
<li>Issue stage 之前的指令都是 in-order 的</li>
<li>Issue stage 之後的指令都是 out-of-order 的，直到到了 commit stage 才會透過 ROB 的資訊來將指令重排回 in-order 的順序</li>
</ul>
</li>
<li>
<p>Issue stage 除了 Issue Queue 外，還有：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image.png"></p>
<ul>
<li>Allocate 電路：用來從 Issue Queue 中找到 free 的 entries，將 register renamed 完的指令加入進 Issue Queue 中</li>
<li>Select 電路 (也稱為 Arbiter 電路)：如果在 Issue Queue 中有多條指令已經準備好了，Select 電路會依照一定的規則，選出最適合的指令送到 FU 中去執行
<ul>
<li>Select 電路是 issue stage 中比較關鍵的元件，會直接影響到 CPU 的執行效率</li>
</ul>
</li>
<li>Wake-up 電路：當一條指令經過 FU 而得到結果時，會通知給 Issue Queue 中所有等待此結果的指令；這些指令對用的 source operands 會被設為 valid 的狀態，這就稱為 wake-up
<ul>
<li>如果一條指令所有的 source operands 都已經準備好了，那該指令就處於 ready 狀態，Select 電路就有可能選擇該指令來 issue</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="811----集中式-centralized-vs-分佈式-distributed">8.1.1 -  集中式 (Centralized) vs. 分佈式 (Distributed)</h2>
<ul>
<li>在 superscalar CPU 中，為了並行執行指令，一般都會有很多的 FU，如有些 FU 負責 integer 的運算，有些 FU 負責乘法與除法運算，有些 FU 負責 memory access
<ul>
<li>如果這些 FU 都共用同一個 Issue Queue，那麼這種架構就稱為 <code>Centralized Issue Queue</code></li>
<li>如果每個 FU 都有一個獨立的 Issue Queue，那麼這種架構就稱為 <code>Distributed Issue Queue</code></li>
</ul>
</li>
<li>Centralized Issue Queue 因為要儲存所有 FU 的指令，因此它的容量需要很大
<ul>
<li>優點：
<ul>
<li>有最大的利用效率，不會浪費 Issue Queue 中任何的空間</li>
</ul>
</li>
<li>缺點：
<ul>
<li>Select 電路和 Wake-up 電路會比較複雜，因為需要從龐大數量的指令中選擇幾條可以被 issued 的指令；這些被選中的指令執行完後還需要 wake up 所有 Issue Queue 中相關的指令，增加了 Select 電路和 Wake-up 電路的面積和 latency</li>
</ul>
</li>
</ul>
</li>
<li>Distributed Issue Queue 由於為每個 FU 都配一個 Issue Queue，因此每個 Issue Queue 的空間可以很小
<ul>
<li>優點：
<ul>
<li>簡化了 Select 電路的設計 (每個 Issue Queue 都有自己的 Select 電路)</li>
</ul>
</li>
<li>缺點：
<ul>
<li>但 Issue Queue 使用率較低，當一個 Issue Queue 滿了以後，即使其他 Issue Queue 還有空間，也無法繼續向其寫入新的指令
<ul>
<li>此時就需要將 pipeline stall，直到這個 Issue Queue 有空出新的空間為止
<ul>
<li>例如：
<ul>
<li>一段時間內執行了大量的 add / sub 指令，則可能由於 add / sub 指令 FU 對應的 Issue Queue 已經滿了而無法處理新的 add / sub 指令
<ul>
<li>這會阻礙新的指令做 register renaming，因為 register renaming stage 仍然是 in-order 的</li>
<li>即使此時別的  FU 的 Issue Queue 仍有空間，也仍需 stall issue stage 之前的 pipeline，降低 Issue Queue 的使用率</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>由於 Issue Queue 是分散的，因此也會增加 Wake-up 電路的複雜度</li>
</ul>
</li>
</ul>
</li>
<li>由於上述兩種設計各有優缺點，因此現代處理器一般都會結合使用上述兩種方法，使某些 FU 共用一個 Issue Queue，某些 FU 有自己獨立的 Issue Queue</li>
</ul>
<h2 id="812---數據捕捉-data-capture-vs-非數據捕捉-non-data-capture">8.1.2 - 數據捕捉 (Data-capture) vs. 非數據捕捉 (Non-data-capture)</h2>
<ul>
<li>
<p>對於 superscalar CPU 而言，還需要考慮在哪個 pipeline stage 讀取 registers 的值</p>
<ul>
<li>何時讀取 registers 對於整個處理器架構有著直接的影響</li>
</ul>
</li>
<li>
<p>一般來說，有兩個時間點可以讀取 registers：</p>
<ul>
<li>
<p><strong>在 issue stage 之前讀取 registers：</strong></p>
<ul>
<li>
<p>這種方法也被稱為<strong>數據捕捉 (Data-capture)</strong> 的架構</p>
</li>
<li>
<p>被 register renamed 後的指令首先會讀取 physical register file，然後將 source registers 的<strong>編號</strong>寫進 Issue Queue，讀到的 source registers 的<strong>值</strong>寫進 <a href="#812---%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-data-capture-vs-%E9%9D%9E%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-non-data-capture"><strong>payload RAM</strong></a></p>
<ul>
<li>如果有 source register 的值在指令寫進 Issue Queue 中時還沒有被計算出來，則該 register 會被標記為 non-available</li>
<li>在之後的 wake-up stage，可以根據 Issue Queue 中所記錄 source registers 編號，透過 bypassing network 取得結果，不需要再讀取 physical register file</li>
</ul>
</li>
<li>
<p>在 Issue Queue 中，儲存指令 operands 的元件稱為 <strong>payload RAM</strong>：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%201.png"></p>
<ul>
<li>Payload RAM 儲存了指令 source registers 的值，當指令從 Issue Queue 中被 select 電路選中時，可以直接從 payload RAM 中對應的 entry 將 source registers 的值給讀出來，並送到 FU 中執行</li>
<li>當一條指令從 Issue Queue 中被選中的同時，它會 broadcast 其 destination register 的編號，Issue Queue 中的其他的指令的 source registers 都會被比較，一旦發現相等時，就會在 payload RAM 中進行標記，等到被選中的指令被計算完畢後，就會透過 bypassing network 將其結果寫進 payload RAM 對應的 entries 中</li>
<li>Payload RAM 負責”捕捉” FU 計算的結果，因此稱為 <strong>data-capture</strong> 架構</li>
<li>Issue Queue 負責比對 registers 編號是否相等，payload RAM 負責儲存 source registers 的值並捕捉對應 FU 的計算結果</li>
</ul>
</li>
<li>
<p>在 superscalar CPU 中，使用 <code>machine width</code> 來表示每個 cycle 實際可以 decode 和 register rename 的指令個數，使用 <code>issue width</code> 來表示每個 cycle 可以被 issued 的指令個數</p>
<ul>
<li>考慮到由於指令間存在 dependencies，很多時候，沒辦法每個 cycle 都將 machine width 個數的指令 issue 到 FU 中執行，只有使 <code>issue width</code> 大於 <code>machine width</code>，才能最大限度地使用 FU
<ul>
<li>因此，現代高性能處理器都會採用多個 FU，盡可能增加每個 cycle 可以被 issued 的指令個數</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Data-capture 架構需要在 issue stage 前讀取 physical register file，因此 physical register file 需要的 <code>read ports 數量 = machine width x 2</code> (假設每條指令只有兩個 source registers)，是直接與 <code>machine width</code> 相關的：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%202.png"></p>
</li>
</ul>
</li>
<li>
<p><strong>在 issue stage 之後讀取 registers：</strong></p>
<ul>
<li>
<p>這種方法也被稱為<strong>非數據捕捉 (Non-data-capture)</strong> 的架構</p>
</li>
<li>
<p>在 non-data-capture 架構中，register renamed 後的指令並不會去讀取 physical register file，而是直接將 source registers 的編號寫進 Issue Queue 中</p>
<ul>
<li>當指令從 Issue Queue 中被 select 電路選中，會使用 source registers 的編號來讀取 physical register file，將讀到的 source registers 的值一併送到 FU 中執行</li>
<li>由於不需要 payload RAM，因此可以增加執行的速度</li>
</ul>
</li>
<li>
<p>由於指令在 issued 後才會讀取 physical register file，因此 physical register file 所需的 <code>read ports 數量 = issue width x 2</code>，是直接與 <code>issue width</code> 相關的，因此所需 physical register file 的 read ports 數量通常比較多</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%203.png"></p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Data-capture 架構：</strong></p>
<ul>
<li>優點：
<ul>
<li>需要的 physical register read ports 數量較少</li>
</ul>
</li>
<li>缺點：
<ul>
<li>需要額外的 payload RAM，硬體面積較大，執行速度較慢</li>
<li>source register 需要經過兩次 reads 和一次 write 的過程，功耗較高
<ul>
<li>如果 source register 的值在指令寫進 Issue Queue 中時已經有被計算出來，那麼就需要將 register 的值從 physical register file 中讀出來，寫進 Issue Queue，然後再從 Issue Queue 中將值讀出來，送到 FU 中</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p><strong>Non-data-capture 架構：</strong></p>
<ul>
<li>優點：
<ul>
<li>在 Issue Queue 中不需要儲存 registers 的值，也不需要使用 payload RAM，硬體面積較小，執行速度較快</li>
<li>Source register 只需要經過一次 read 即可，功耗較低
<ul>
<li>從 physical register file 讀出來送進 FU 中</li>
</ul>
</li>
</ul>
</li>
<li>缺點：
<ul>
<li>Physical register file 需要比較多的 read ports</li>
</ul>
</li>
</ul>
</li>
<li>
<p>這兩種架構的選擇，決定了 ROB 的實做方式：</p>
<ul>
<li>
<p>當<a href="../superscalar-overview-ch7/#721---%E4%BD%BF%E7%94%A8-rob-%E4%BE%86%E5%AF%A6%E5%81%9A%E6%9A%AB%E5%AD%98%E5%99%A8%E9%87%8D%E5%91%BD%E5%90%8D">使用 ROB 做 register renaming</a> 時 (i.e. <strong>使用 ROB 當作 PRF</strong>)，一般都會配合使用 <a href="#812---%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-data-capture-vs-%E9%9D%9E%E6%95%B8%E6%93%9A%E6%8D%95%E6%8D%89-non-data-capture"><strong>data-capture</strong></a> 的架構</p>
<ul>
<li>在一條指令 retire 前，其 architecture register 的計算結果會被存在 ROB 中，register renaming table 的 pointer 會指向 ROB；當這條指令 retire 時，其 architecture register 的計算結果會從 ROB 移到 ARF，RAT 的 pointer 也會一併更新指向 ARF</li>
<li>採用 data-capture 架構在讀取 source register 的值寫進 Issue Queue 時，可以直接透過 RAT 找到對應的 physical register，因此不用考慮位置的變化</li>
<li>反之，採用 non-data-capture 的架構，當指令從 Issue Queue 中被 select 電路選中，要讀取 PRF 來將 source registers 的值送到 FU 中執行時，並沒辦法直接使用 Issue Queue 中記錄的 source register 的編號 (physical register number) 來讀取 ROB 取得 source register 的值 (因為計算結果有可能已經被搬進 ARF 中了），因此需要額外的處理</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="813---壓縮-vs-非壓縮">8.1.3 - 壓縮 vs. 非壓縮</h2>
<ul>
<li>根據 Issue Queue 的工作方式，又可將其分為<strong>壓縮 (Compressing)</strong> 和<strong>非壓縮 (Non-compressing)</strong> 兩種架構</li>
<li><strong>Compressing Issue Queue：</strong>
<ul>
<li>
<p>每當一條指令被選中並從 Issue Queue 中移除時，會出現一個”空格”，這條指令上面的所有指令都會下移一格，將這條指令的空格給補上：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%204.png"></p>
</li>
<li>
<p>透過這種方式，可以保證 Issue Queue 中的 empty entries 都是在 Issue Queue 中的 tail</p>
<ul>
<li>當有新的指令寫入時，只需將其寫入 Issue Queue 中的 tail 即可</li>
</ul>
</li>
<li>
<p>要實現這樣的架構，需要在每個 Issue Queue 的 entry 前面加上 multiplexer，用來移動一個 entry 的內容</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%205.png"></p>
</li>
<li>
<p>優點：</p>
<ul>
<li>Allocation 電路較簡單，Issue Queue 中的 empty entries 都是在 Issue Queue 中的 tail，只需使用 Issue Queue 的 write pointer 即可得知指令寫入 Issue Queue 的位置</li>
<li>Select 電路設計比較簡單
<ul>
<li>
<p>通常為了讓處理器可以最大限度的同時執行多條指令，一般都是從所有準備好的指令中，優先選擇最老的指令到 FU 執行，也就是 <code>oldest-first</code> 方法</p>
<ul>
<li>最老的指令通常會與其他指令之間有更多的 RAW dependencies，因此可以 wake up 更多的指令</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%206.png"></p>
</li>
<li>
<p>Compressing Issue Queue 中的指令已經是照著 oldest → newest 的順序排好的，因此 select 電路可以很容易依照 oldest-first 的方法選出指令來執行</p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>缺點：</p>
<ul>
<li>Issue Queue 的設計較複雜
<ul>
<li>需要額外加入 multiplexer 用來移動一個 entry 的內容</li>
</ul>
</li>
<li>Select 電路的 latency 與 Issue Queue 的容量成正比， 如果 Issue Queue 容量越大，就會使 select 電路的 latency 越長
<ul>
<li>雖然可以很容易的找出最老的指令，但因為要跟所有的 Issue Queue entries 比較才能找出要被 issue 的指令，因此 Issue Queue 容量越大，select 電路的 latency 越長</li>
</ul>
</li>
<li>同時間會有多個指令需要被移動，因此功耗也會比較高</li>
</ul>
</li>
</ul>
</li>
<li><strong>Non-compressing Issue Queue：</strong>
<ul>
<li>每當一條指令被選中並從 Issue Queue 中移除時，Issue Queue 中的其他指令並不會被搬移，而是會停留在原本的位置，i.e. 沒有壓縮的過程
<ul>
<li>無法根據指令在 Issue Queue 中的位置來決定指令的新舊</li>
</ul>
</li>
<li>實務上沒辦法把所有 Issue Queue 的指令都掃過一遍，找出最老已經準備好的指令；因此可以採用折衷的方式，從 Issue Queue 的 head 開始尋找，直到找到一條已經準備好的指令就直接選擇它
<ul>
<li>這種 select 電路的實做相對簡單，但由於沒辦法找出 oldest-first 的指令，因此沒辦法獲得最好的效能</li>
</ul>
</li>
<li>優點：
<ul>
<li>指令不用每個 cycle 都需要移動，功耗較低</li>
<li>少了 multiplexer，硬體面積較小</li>
</ul>
</li>
<li>缺點：
<ul>
<li>如果真的要實做 oldest-first 的功能，就需要更複雜的 select 電路設計 (參考：<a href="#84---%E4%BB%B2%E8%A3%81-arbitration">8.4 - 仲裁 (Arbitration)</a>)，會增加 latency</li>
<li>Allocation 電路設計較複雜，需要掃過 Issue Queue 中所有的 entries，才能找到 empty 的 entry 來將指令寫入；尤其 superscalar CPU 每個 cycle 都會寫入多條的指令到 Issue Queue 中，allocation 的電路設計會更為複雜</li>
</ul>
</li>
</ul>
</li>
<li><strong>Compressing Issue Queue</strong> 和 <strong>Non-compressing Issue Queue</strong> 設計各有優缺點，在現實世界的 superscalar CPU 中都有採用</li>
</ul>
<h1 id="82---發射過程的流水線">8.2 - 發射過程的流水線</h1>
<h2 id="821---非數據捕捉-non-data-capture-架構的流水線">8.2.1 - 非數據捕捉 (Non-data-capture) 架構的流水線</h2>
<ul>
<li>
<p>一條在 Issue Queue 中的指令需等到以下條件都成立後，才可以被 issued 到 FU 中執行：</p>
<ul>
<li>這條指令所有的 source registers 都準備好了</li>
<li>這條指令能從 Issue Queue 中被選中，也就是需要被 select 電路選中才能被 issued</li>
<li>需要能從 registers、payload RAM 或 bypassing network 中獲得 source registers 的值</li>
</ul>
</li>
<li>
<p>對於一個 source register 來說，如果被寫到 Issue Queue 時，還是 not-ready 的狀態，等到之後的某個時間，它變成 ready 的狀態，這個過程便稱為 <strong>Wake-up</strong></p>
<ul>
<li>需通過 bypassing network 才可以通知到 Issue Queue 中每個 source registers</li>
</ul>
</li>
<li>
<p>Wake-up 的過程：</p>
<ul>
<li>
<p>最簡單的作法：當一條指令在 FU 得到其運算的結果時，將 Issue Queue 中使用到這個計算結果的所有 source registers 都設為 ready 的狀態：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%207.png"></p>
<ul>
<li>Issue 被分為了 <strong>wake-up</strong> 和 <strong>select</strong> 兩個 stages
<ul>
<li>在 wake-up stage，Issue Queue 中所有得到計算結果的 source registers 都會被改為 ready 狀態</li>
<li>在 select stage，select 電路會從 Issue Queue 中選擇一條最合適的指令 issue 到 FU 中</li>
</ul>
</li>
<li>然後，這樣 RAW 相關的兩條指令，並沒辦法 back-to-back 的執行，中間間隔了 3 個 cycles，執行效率不佳</li>
<li>這種指令執行完才 wake up 相關的指令的方法便是：<strong>Tomasulo</strong></li>
</ul>
</li>
</ul>
</li>
<li>
<p>為了提高執行效率，可以將 wake-up 的過程提前，以便可以 back-to-back 執行指令：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%208.png"></p>
<ul>
<li>需要透過 bypassing network 才能 back-to-back 執行指令
<ul>
<li>當指令 B 到 execute stage 時，就可以透過 bypassing network 得到指令 A 的執行結果，便可以被 issued 到 FU 中執行了</li>
</ul>
</li>
</ul>
</li>
<li>
<p>實際上，指令 A 的 select 和指令 B 的 wake-up 這兩個操作是在同一個 cycle 中接續執行的</p>
<ul>
<li>
<p>只有當 select 電路選出可以 issue 的指令後，才可以對 Issue Queue 中相關的 source registers 進行 wake up，只有兩個操作是在<strong>同一個 cycle 內完成</strong>，才可以 back-to-back 執行兩條 RAW 的指令：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%209.png"></p>
</li>
<li>
<p>如果 select 和 wake-up 分為 2 個不同的 cycles，那麼就無法 back-to-back 執行兩條 RAW 的指令：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2010.png"></p>
<ul>
<li>指令 A 被選中，但要等到下一個 cycle 才能 wake up 指令B，那麼指令 A 和指令 B 之間的執行就會相差一個 cycle</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Select 電路和 wake-up 電路都是相對比較複雜的，將它們放在同一個 cycle 內執行，肯定會使 cycle time 變長，降低 CPU 的頻率；而將這兩個電路分成兩個不同的 cycles，可以降低 cycle time，提昇 CPU 的頻率，但會降低 CPU 的 IPC</p>
<ul>
<li>將 select 和 wake-up 分成兩個不同的 cycles，IPC 約會下降 10% ~15%
<ul>
<li>此外，由於增加了一個 cycle，因此也會帶來其他的負面影響：
<ol>
<li>Miss-prediction penalty 的增加</li>
<li>Cache 存取 cycles 數的增加
<ul>
<li>Cache 存取的時間是固定的，cycle time 變短，cache 存取需要的 cycles 數就變多了</li>
</ul>
</li>
<li>增加了一個 cycle，需要更多的硬體資源，功耗變大</li>
</ol>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Superscalar CPU 中存在多個 FU，每個 FU 的執行時間都是不同的；即使是同一個 FU，不同指令所需的執行時間也不一定相同</p>
<ul>
<li>
<p>當一條指令的 FU 沒辦法在一個 cycle 內完成，pipeline 的 execute stage 的執行就需要超過一個 cycle，有可能就來不及 bypass 結果給 RAW 的指令</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2011.png"></p>
<ul>
<li>
<p>指令 A 是乘法指令，在 FU 中需要 2 個 cycles 才能計算出結果，如果仍在指令 A 被 selected 時同時 wake up 指令 B，等到指令 B 進到 execute stage 時，由於指令 A 的結果在那個時候還沒有被計算出來，指令 B 就沒辦法執行</p>
</li>
<li>
<p>因此，需要將指令 B 的 wake-up delay 一個 cycle，才可以讓指令 B 進到 execute stage 時，透過 bypassing network 得到指令 A 的計算結果</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2012.png"></p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>除此之外，像是 load/store 指令，其 execute 所需要的 cycles 是取決於 D-Cache 或 Store buffer 是否 hit/miss 的，都需要做特別的處理</p>
</li>
</ul>
<h2 id="822---數據捕捉-data-capture-架構的流水線">8.2.2 - 數據捕捉 (Data-capture) 架構的流水線</h2>
<ul>
<li>
<p>Data-capture 跟 non-data-capture 架構最大的差異就是使用了 payload RAM 來儲存所有指令的 source registers 的值，當指令被 issued 時，可以直接得到 source registers 的值，而不須再去讀取 physical register file</p>
<ul>
<li>相比於 non-data-capture 架構少了 <code>Regfile read</code> 的 stage</li>
</ul>
</li>
<li>
<p>Data-capture 架構同樣需要分為 select 和 wake-up 兩個階段，只不過指令在被 select 電路選中後，不需要再去讀取 physical register，而是可以直接讀取 payload RAM 取得 source registers 的值</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2013.png"></p>
<ul>
<li>一條指令被 select 後 (如：指令 A)，同個 cycle 也可以 wake up 其他有 dependency 的指令 (如：指令 B、C)；同時還會去讀取 payload RAM，得到 source registers 的值
<ul>
<li>Payload 和 wake-up 是在同一個 cycle 中發生的</li>
</ul>
</li>
<li>在得到 source registers 的值後，下一個 cycle 就可以進到 execute stage，將指令送到 FU 中執行</li>
<li>被 wake up 的指令 (如：指令 B、C)，可以在其 execute stage 透過 bypassing network 取得其所依賴的指令的計算結果 (如：指令 B)，也可以從 payload RAM 得到計算結果 (如：指令 C)
<ul>
<li>對於指令 C 來說，在其 execute stage 時，指令 A 已經將其計算結果寫回 payload RAM 了，因此可以直接從 payload RAM 讀取指令 A 的計算結果 (但仍需要將指令 B 的計算結果 bypass 給指令 C)</li>
</ul>
</li>
<li>此設計將 select 和讀取 payload RAM 放到同一個 cycle 內，因此在一個 cycle 內，需要同時讀取 payload RAM，又需要寫入 payload RAM，因此需要使用 multi-port 的設計，讓 cycle time 變得過大
<ul>
<li>如：<code>*Cycle i+2*</code>，需要將指令 A 的計算結果寫回 payload RAM；指令 C 又需要同時讀取 payload RAM 得到指令 A 的計算結果</li>
</ul>
</li>
<li>同時，當 FU 的個數比較多時，FU 的計算結果也需要透過 bypassing network 來傳遞，因此也會佔用不少的硬體支援和過長的執行時間，因此可以進一步將 pipeline 進行細分</li>
</ul>
</li>
<li>
<p>以下的設計將 payload RAM 的存取獨立成一個單獨的 pipeline stage，以避免 multi-port payload RAM 影響 CPU 的 cycle time：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2014.png"></p>
<ul>
<li>此外，在 execute stage，FU 得到計算結果後，也不會在同一個 cycle 就 bypass 結果給其他的指令，而是將 bypass 放到下一個 pipeline stage
<ul>
<li>在 bypass stage 中，會同時將計算結果寫進 payload RAM 以及 bypass 給其他指令</li>
</ul>
</li>
<li>由於指令 C 依賴指令 A 和指令 B 的計算結果，因此需要等到 <code>*Cycle i+1*</code> 才能真的被 wake up</li>
</ul>
</li>
<li>
<p>有些設計甚至還會將 select 和 wake-up 拆分成兩個不同的 stages，但缺點就是 RAW 相關的指令無法 back-to-back 的被執行</p>
</li>
</ul>
<h1 id="83---分配-allocation">8.3 - 分配 (Allocation)</h1>
<ul>
<li>
<p>Allocation 電路其實是屬於 dispatch stage，而不是 issue stage 的</p>
</li>
<li>
<p>採用 <a href="#813---%E5%A3%93%E7%B8%AE-vs-%E9%9D%9E%E5%A3%93%E7%B8%AE">Non-Compressing Issue Queue</a> 設計的 Issue Queue，其 empty entries 的分佈是沒有規律的；對於每個 cycle 可以 decode <em>N</em> 條指令的 superscalar CPU 來說，最理想的狀態就是可以從 Issue Queue 中找到 <em>N</em> 個 empty entries，需要 allocation 電路掃過整個 Issue Queue</p>
<ul>
<li>Allocation 所需要的時間，與 Issue Queue 的容量，以及每個 cycle 要寫入的指令個數成正比</li>
</ul>
</li>
<li>
<p>可以採用一個 free list 來記錄所有 Issue Queue 中 empty entries 的編號，每個 cycle 都從這個 free list 中讀出 <em>N</em> 個編號，就可以找到對應的 Issue Queue empty entries 了</p>
<ul>
<li>這個 free list 可以透過 FIFO 來實現</li>
<li>然而，這種方法仍有一定的複雜度，產生的 latency 也不一定會比較小
<ul>
<li>如果 <em>N</em> 很大，就需要在一個 cycle 內 pop 出 <em>N</em> 個 FIFO entries</li>
</ul>
</li>
</ul>
</li>
<li>
<p>一種折衷的方法，可以直接將 Issue Queue 分成 <em>N</em> 段，從每段找出一個 empty entry</p>
</li>
<li>
<p>如 <em>N = 4</em>：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2015.png"></p>
<ul>
<li>每段都會有一個獨立的 allocation 電路，這樣一個容量為 12 個 entries 的 Issue Queue，每段只需要從 3 個 entries 中找到 1 個 empty entry 即可</li>
<li>每個獨立的 allocation 電路都可以同時工作，所需查找的 entries 數也很少，因此 latency 很小</li>
<li>缺點：
<ul>
<li>
<p>如果一個段中沒有 empty entry 了，那麼就算其他段有多餘的 empty entry，這個段也沒辦法接收新的指令</p>
</li>
<li>
<p>更糟糕的是，在 register renaming stage，指令仍然是 in-order 的，因此如果一條指令沒辦法找到 Issue Queue 的 empty entry，其後的指令就算其對應的段仍有 empty entries，也無法被寫入 Issue Queue 中：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2016.png"></p>
<ul>
<li>指令 A 對應的段已經沒有 empty entries 了，無法被寫入 Issue Queue 中；但由於此時仍是 in-order 的，因此會導致縱使指令 B、C、D 的段有 empty entris，一樣無法被寫入 Issue Queue 中</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Allocation、select、wake-up 是 issue stage 中三個主要的步驟，每個步驟都需要做很多的事情，因此 issue stage 是 superscalar CPU 中工作量比較繁忙的 pipeline stage</p>
</li>
</ul>
<h1 id="84---仲裁-arbitration">8.4 - 仲裁 (Arbitration)</h1>
<h2 id="841---1-of-m-的仲裁電路">8.4.1 - 1-of-M 的仲裁電路</h2>
<ul>
<li>前面的章節有提到，<a href="#813---%E5%A3%93%E7%B8%AE-vs-%E9%9D%9E%E5%A3%93%E7%B8%AE">Non-compressing Issue Queue</a> 需要特別的處理才能實做 oldest-first 的功能，不能單純的由指令在 Issue Queue 中的位置來判斷</li>
<li>越舊的指令，和它存在 dependency 的指令就越多，因此優先 select 最舊的指令，則有機會可以wake up 更多的指令；除此之外，最舊的指令還佔據著處理器的其他資源，如 ROB 和 store buffer… etc，越早執行這些舊的指令，就可以越早釋放這些資源來讓更新的指令使用</li>
<li>要識別出 Issue Queue 中哪些指令是最舊的，就需要知道這些指令的年齡，也就是進到 pipeline 的先後順序
<ul>
<li>In-order core 的年齡資訊很容易判斷，在 pipeline 中先執行的指令肯定比後執行的指令來得老</li>
<li>Out-of-order core 的年齡資訊必須依靠 ROB 來判斷，可以使用指令在 ROB 中的 index 來判斷指令的年齡先後 (指令是從 ROB index 0 開始寫)
<ul>
<li>
<p>ROB 是一個 circular FIFO，因此沒辦法直接拿 ROB 的 index 來判斷，需要做一些處理</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2017.png"></p>
<ul>
<li>
<p>Write pointer 和 read pointer 在同一個 lap 上，可以直接比較 write pointer 和 read pointer 的 index 來識別指令的年齡</p>
</li>
<li>
<p>但當 write pointer 和 read pointer 不在同一個 lap 上 (i.e. write pointer 比 read pointer 多 wrap 了一次)，那就沒辦法透過直接比較 write pointer 和 read pointer 的 index 來識別指令的年齡</p>
</li>
<li>
<p>因此需要額外加 1 個 position bit，每次 write 或 read pointer wrap 時，write pointer 或 read pointer 所指到的 position bit 都會被 toggled (0 → 1、1 → 0)</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2018.png"></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2019.png"></p>
<ul>
<li>當  write pointer 指到的 position bit 與 read pointer 所指到的 position bit <strong>相同</strong>時 (i.e. write pointer 和 read pointer 在同一個 lap 上)，<strong>指令的 ROB index 越小，年齡越老</strong></li>
<li>當  write pointer 指到的 position bit 與 read pointer 所指到的 position bit <strong>不同</strong>時 ****(i.e. write pointer 比 read pointer 多 wrap 了一次)，<strong>指令的 ROB index 越大，年齡越老</strong></li>
</ul>
</li>
<li>
<p>為了記錄年齡資訊，需要將每條指令在 ROB 中的 index (i.e. 年齡) 也一併寫進 Issue Queue 中，這樣在 Issue Queue 中每條指令就有了年齡的資訊，select 電路就可以依據這個資訊，從所有準備好的指令中找到最舊的那個指令</p>
</li>
<li>
<p>如果 Issue Queue 中有 <em>M</em> 個 entry，這種 select 電路就稱為 <strong>1-of-M select 電路</strong></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2020.png"></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2021.png"></p>
<ul>
<li>
<p>Issue Queue 中每條指令都由 1 個 ready bit 來表示該指令是否已經準備好了：</p>
<ul>
<li>兩個指令的 ready bits 皆為 1 時：選擇年紀比較大 (i.e. ROB index 比較小) 的指令</li>
<li>只有一個指令的 ready bit 為 1 時：選擇 ready bit 為 1 的那條指令</li>
<li>兩個指令的 ready bits 皆為 0 時：選擇哪條指令都無所謂，因為下一級也不會被選中</li>
</ul>
</li>
<li>
<p>實際上，可以將每條指令在 Issue Queue 中的 index 一起加入上述的比較電路，這樣就可以在取得年齡值時，同時取得該指令在 Issue Queue 中的 index，進而找到 Issue Queue 中這個年齡值所對應的指令</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2022.png"></p>
<ul>
<li>新增的 multiplexer 是跟原有的同步執行的，因此不會影響到 CPU 的 latency</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="842---n-of-m-的仲裁電路">8.4.2 - N-of-M 的仲裁電路</h2>
<ul>
<li>
<p>多個 FU 有可能會共用同一個 Issue Queue，因此需要在 1 個 cycle 內為每個 FU 都選出一條指令，也就代表需要使用 <strong>N-of-M 的 select 電路</strong></p>
<ul>
<li>N：FU 個數</li>
<li>M：Issue Queue 的容量</li>
</ul>
</li>
<li>
<p>假設一 2-of-M select 電路，如果要遵循 oldest-first 的設計，每個 cycle 需要選出 2 條指令來：</p>
<ul>
<li>需要兩級的 select 電路，第一級 select 電路選出指令後，將該指令標記，這樣第二級 select 電路在挑選時就會排除第一級 select 電路所選出來的指令</li>
<li>但這樣的實做，其 latency 會是 1-of-M select 電路的兩倍，因此不可能擴展到 N-of-M select 電路</li>
</ul>
</li>
<li>
<p>因此，要設計一個 N-of-M select 電路，仍然需要使用折衷的方法，在性能、電路面積和速度之間找到一個平衡點：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2023.png"></p>
<ul>
<li>四個 ALU 共用同一個 Issue Queue，假設 Issue Queue 容量為 M，每個 FU 都有一個專屬的 1-of-M select 電路</li>
<li>Issue Queue 中的指令會依照指令類型，分配給對應的 FU；如果存在相同功能的 FU，則會按照 round-robin 或是 random 的方式分配指令給這幾個 FU</li>
<li>可以透過 demultiplexer 來實現
<ul>
<li>因為 Issue Queue 中每個 entry 都有可能會放不同類型指令，因此每個 FU 的 select 電路都會有 M 個輸入，執行完整的 1-of-M select</li>
</ul>
</li>
<li>整個 N-of-M select 電路的 latency 可以降到跟 1-of-M select 電路一樣 (demultiplexer 的 latency 很小，可以忽略)</li>
</ul>
</li>
<li>
<p>然而，這樣的設計無法實現理想的 N-of-M 功能：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2024.png"></p>
<ul>
<li>
<p><code>LOAD</code>、<code>DIV</code>、<code>MUL</code>、<code>SUB</code>、<code>ADD</code> 指令皆 ready，但只有 <code>LOAD</code>、<code>DIV</code>、<code>ADD</code> or <code>SUB</code> or <code>ADD</code> 其中一條指令可以被 issued，因此 4-of-M 的 select 電路，最後只能 issue 三條指令到 FU 中執行</p>
</li>
<li>
<p>為了解決這樣的問題，可以增加 FU 的數量，如使用兩個相同的 ALU：<code>ALU0</code> 和 <code>ALU1</code></p>
<ul>
<li>但會引入新的問題，一條 ALU 指令究竟要分配給哪個 ALU?</li>
</ul>
</li>
<li>
<p>可以採用一種 round-robin 的分配方法：</p>
<ul>
<li>目前 cycle 寫進 Issue Queue 的指令全部 issue 給 <code>ALU0</code>、下一個 cycle 寫進 Issue Queue 的指令改全部 issue 給 <code>ALU1</code></li>
<li>可以透過一個額外的 1 bit 訊號來標記這個 cycle 指令的 issue 狀態，且這個 1 bit 訊號每個 cycle 都會 toggle (<code>0</code> → <code>1</code>；<code>1</code> → <code>0</code>)
<ul>
<li><code>0</code>：ALU 類型的指令會被 issued 給 <code>ALU0</code></li>
<li><code>1</code>：ALU 類型的指令會被 issued 給 <code>ALU1</code></li>
</ul>
</li>
</ul>
</li>
<li>
<p>不過這仍然無法保證嚴格的 oldest-first：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2025.png"></p>
<ul>
<li><code>XOR</code> 和 <code>AND</code> ⇒ 只有 <code>AND</code> 可以被 issued 進 <code>ALU1</code></li>
<li><code>SUB</code> 和 <code>ADD</code> ⇒ 只有 <code>ADD</code> 可以被 issued 進 <code>ALU0</code></li>
<li>但 <code>SUB</code> 比 <code>AND</code> 還老，但 <code>AND</code> 卻比 <code>SUB</code> 先被執行</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch8-part1/image%2026.png"></p>
<ul>
<li><code>CMP</code>、<code>SUB</code> 和 <code>ADD</code> ⇒ 只有 <code>AND</code> 可以被 issued 進 <code>ALU0</code></li>
<li>雖然 <code>ALU1</code> 也可以用來計算，但此時卻沒有任何指令可以被 issued 進 <code>ALU1</code>，形成浪費</li>
</ul>
</li>
</ul>
</li>
<li>
<p>現代處理器大多一個 cycle 可以執行 4 ~ 6 條指令</p>
</li>
<li>
<p>需要將多個不同類型的指令加以合併使用同一個 FU：</p>
<ul>
<li>加減法、邏輯運算、移位運算 ⇒ 傳統意義上的 ALU</li>
<li>整數乘法、整數除法</li>
<li>浮點數運算</li>
<li>Memory access、Co-processor access</li>
</ul>
</li>
<li>
<p>實際的設計中，需要對不同的指令集，甚至不同的程式進行分析，才能對 FU 進行合理的分類，得到相對優化的分配結果</p>
</li>
<li>
<p>由於 select 電路的 latency 與 Issue Queue 的容量成正比；當越多的 FU 共用同一個 Issue Queue 時，Issue Queue 的容量也需要相對應的增加，因此就會增加 select 電路的 latency</p>
<ul>
<li>但如果是想使越少的 FU 共用同一個 Issue Queue，那麼就需要更多數量的 Issue Queue，此時 wake-up 就需要更多的佈線和 comparators，反而增加 wake-up 電路的 latency 和功耗</li>
<li>因此如何配置 Issue Queue 的分配並沒有一個標準答案，需要根據實際的需求並分析</li>
</ul>
</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
