<?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>Branch Prediction on 0xc0de</title>
    <link>https://0xc0de.xyz/tags/branch-prediction/</link>
    <description>Recent content in Branch Prediction 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/branch-prediction/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; 第 4 章 - 分支預測 (Part 2)</title>
      <link>https://0xc0de.xyz/posts/superscalar-overview-ch4-part2/</link>
      <pubDate>Fri, 14 Mar 2025 00:00:00 +0800</pubDate>
      <guid>https://0xc0de.xyz/posts/superscalar-overview-ch4-part2/</guid>
      <description>&lt;h1 id=&#34;43---分支指令的目標位址預測&#34;&gt;4.3 - 分支指令的目標位址預測&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;分支指令的目標位址可以分為兩種：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接跳轉 (direct)：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;對於直接跳轉指令 (e.g. RISC-V 的 &lt;code&gt;beq&lt;/code&gt;、&lt;code&gt;jal&lt;/code&gt;、&lt;code&gt;lui&lt;/code&gt; 指令)，它的跳轉 offset 是以 immediate value 的方式 encode 在 opcode 中 (有可能是 PC-relative，也有可能不是)，所以它的目標位址是固定的，只要記錄這條分支指令目標位址即可&lt;/li&gt;
&lt;li&gt;當再次遇到這條分支指令時，如果分支預測結果是 taken，那麼目標位址就可以直接使用先前所記錄的值&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;間接跳轉 (indirect)：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;對於間接跳轉指令 (e.g. RISC-V 的 &lt;code&gt;jalr&lt;/code&gt; 指令)，由於它的目標位址來自於暫存器，而暫存器的值是有可能一直變化的，所以對間接跳轉指令來說，要預測目標位址並不是一件容易的事情&lt;/li&gt;
&lt;li&gt;然而慶幸的是，程式中大部份的間接跳轉指令都是用來呼叫函式的，也就是 call 和 return；這種目標位址是有規律的，因此可以對其進行預測&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;431---直接跳轉類型的分支預測&#34;&gt;4.3.1 - 直接跳轉類型的分支預測&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;對於直接跳轉類型的分支指令 (假設是 PC-relative)，其目標位址有兩種情況：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;當分支指令 not taken 時：
&lt;ul&gt;
&lt;li&gt;目標位址 =  當前分支指令的 PC 值 + sizeof(fetch group)
&lt;ul&gt;
&lt;li&gt;e.g. PC + 4&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;當分支指令 taken 時：
&lt;ul&gt;
&lt;li&gt;目標位址 = 當前分支指令的 PC 值 + signed extended offset&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;e.g. PC + offset&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="43---分支指令的目標位址預測">4.3 - 分支指令的目標位址預測</h1>
<ul>
<li>分支指令的目標位址可以分為兩種：
<ul>
<li><strong>直接跳轉 (direct)：</strong>
<ul>
<li>對於直接跳轉指令 (e.g. RISC-V 的 <code>beq</code>、<code>jal</code>、<code>lui</code> 指令)，它的跳轉 offset 是以 immediate value 的方式 encode 在 opcode 中 (有可能是 PC-relative，也有可能不是)，所以它的目標位址是固定的，只要記錄這條分支指令目標位址即可</li>
<li>當再次遇到這條分支指令時，如果分支預測結果是 taken，那麼目標位址就可以直接使用先前所記錄的值</li>
</ul>
</li>
<li><strong>間接跳轉 (indirect)：</strong>
<ul>
<li>對於間接跳轉指令 (e.g. RISC-V 的 <code>jalr</code> 指令)，由於它的目標位址來自於暫存器，而暫存器的值是有可能一直變化的，所以對間接跳轉指令來說，要預測目標位址並不是一件容易的事情</li>
<li>然而慶幸的是，程式中大部份的間接跳轉指令都是用來呼叫函式的，也就是 call 和 return；這種目標位址是有規律的，因此可以對其進行預測</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="431---直接跳轉類型的分支預測">4.3.1 - 直接跳轉類型的分支預測</h2>
<ul>
<li>
<p>對於直接跳轉類型的分支指令 (假設是 PC-relative)，其目標位址有兩種情況：</p>
<ol>
<li>當分支指令 not taken 時：
<ul>
<li>目標位址 =  當前分支指令的 PC 值 + sizeof(fetch group)
<ul>
<li>e.g. PC + 4</li>
</ul>
</li>
</ul>
</li>
<li>當分支指令 taken 時：
<ul>
<li>目標位址 = 當前分支指令的 PC 值 + signed extended offset</li>
</ul>
</li>
</ol>
</li>
<li>
<p>e.g. PC + offset</p>
</li>
<li>
<p>由於直接跳轉的分支指令，其跳轉的 offset 以 immediate value 的方式 encode 在 opcode 中的，所以 offset 是不會發生變化的，因此對於此類型的指令進行分支預測是很容易的</p>
<ul>
<li>只需使用一個表格，記錄下每條分支指令的目標位址即可；當一條分支指令執行且預測為 taken 時，直接查詢該表格即可得知該分支指令的目標位址</li>
</ul>
</li>
<li>
<p>此表格也就是所謂的 <code>BTB (Branch Target Buffer)</code>，本質上就是一種 cache，使用 PC 值的一部分作為 index，PC 值的另外一部份作為 tag (i.e. partial-tag)</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image.png"></p>
<ul>
<li>BTB 中存放多條的 <code>BTA (Branch Target Address)</code></li>
<li>因為 index 部份相同的分支指令會 index BTB 中同一條 BTA，因此還需要使用 tag 來判斷是否 hit 或 miss</li>
<li>同樣的，由於 BTB 大小有限，當發生 conflict 時，就需要將原本的 BTA 給 evict 出去，然後將新的 BTA 寫入 BTB</li>
</ul>
</li>
<li>
<p>BTB 也可以採用 set-associative 的設計，當有多條 index 相同的分支指令時，可以將它們的 BTA 寫到不同的 ways 中</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%201.png"></p>
</li>
<li>
<p>Set-associative 的 BTB 會增加硬體設計的複雜度，使 BTB 的佔用的硬體面積變大，同時降低了 BTB 的訪問速度，因此現實的處理器中，BTB 的 way 數通常都會比較小</p>
</li>
<li>
<p>除了使用 PC 值的一部分作為 partial-tag，也可以採用其他的方法 (如：XOR) 來 encode tag，降低 tag 的 bits 數：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%202.png"></p>
<ul>
<li>此方法將 28 bits 的 PC 值，每相鄰的 4 個 bits 做 XOR，最後得到 14 bits 的 tag 值</li>
</ul>
</li>
<li>
<p>一般情況下，為了最大限度的利用 BTB 有限的空間，只會將發生 taken 的分支指令的目標位址存入 BTB 中；對於 not taken 的分支指令，其目標位址就是下一條指令的位址，因此不需要特別做處理</p>
</li>
<li>
<p>當一條分支指令發生 BTB miss 或是 BTB conflict 時，可以採用以下兩種方式來處理：</p>
<ol>
<li>
<p>停止執行：</p>
<ul>
<li>
<p>當發生 BTB miss 或是 BTB conflict 時，暫停 instruction fetch (i.e. 產生 bubbles，也就是 pipeline stall)，直到這條分支指令的目標位址被算出來為止</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%203.png"></p>
<ul>
<li>
<p>在 Cycle <em>i</em> (fetch stage) 的時候發現分支指令的 PC，但在 BTB 中找不到對應的 BTA，發生 BTB miss，那麼就得暫停後續的 instruction fetch，直到分支指令從 I-Cache 中讀取出來並 decode (Cycle <em>i + 2</em>)，因此會引入兩個 bubbles，在 Cycle <em>i + 3</em> 才可以再根據預測目標位址 fetch 下一道指令</p>
<ul>
<li>如果對 I-Cache 的訪問需要比較多的 cycles，就會引入更多的 bubbles</li>
</ul>
</li>
<li>
<p>對於直接跳轉的分支指令，其目標位址可以在 decode stage 就得知其跳轉的 offset，此時就可以將目標位址給計算出來：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%204.png"></p>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>繼續執行：</p>
<ul>
<li>發生 BTB miss 的時候，不將 pipeline 給停下來，而是直接繼續使用下一道指令的位址來 fetch instruction (e.g. PC + 4)</li>
<li>當最後分支指令的目標位址計算出來後，如果發現目標位址跟下一道的指令位址不同，則將所有分支指令之後的指令都從 pipeline 中給 flush 掉，並重新使用計算出來後的目標位址來 fetch instruction</li>
<li>由於分支指令也有可能是 not taken，所以直接繼續使用下一道指令的位址來 fetch instruction，而不停下 pipeline，也是有可能是正確的</li>
<li>但從功耗的觀點來看，這樣做是更浪費功耗的，所以如果是採用此方法，對於功耗比較敏感的設計並不是一個好的選擇</li>
</ul>
</li>
</ol>
</li>
</ul>
<h2 id="432---間接跳轉類型的分支預測">4.3.2 - 間接跳轉類型的分支預測</h2>
<ul>
<li>對於間接跳轉的分支指令來說，其目標位址是存在暫存器中的，且是經常變化的，所以無法透過 BTB 的方式來對目標位址進行準確的預測</li>
<li>所幸的是，大部分的間接跳轉的分支指令都是用來呼叫函式的，也就是 call 和 return；這種目標位址是有規律的，因此可以對其進行預測</li>
</ul>
<ol>
<li>Call 和 return 指令的分支預測：
<ul>
<li>
<p>對於程式中一條 call 指令，其每次呼叫的函式都是固定的，也代表其目標位址都是固定的，因此可以透過 BTB 來對 call 指令來進行預測：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%205.png"></p>
</li>
<li>
<p>但對於 return 指令而言，有可能會 return 到不同的目標位址</p>
<ul>
<li>E.g. <code>printf()</code> 被不同的函式呼叫</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%206.png"></p>
</li>
<li>
<p>根據 return 指令的特點，可以設計一個暫存器，保存最近執行的 call 指令的下一條指令的位址；這個暫存器是 LIFO 的，也就符合函式呼叫和返回的特點</p>
<ul>
<li>
<p>這個暫存器的工作原理和 stack 是一樣的，稱為：<code>RAS (Return Address Stack)</code></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%207.png"></p>
</li>
</ul>
</li>
<li>
<p>綜合起來，可以使用 <strong>BTB</strong> 對 <strong>call 指令</strong>進行預測，而使用 <strong>RAS</strong> 對 <strong>return 指令</strong>進行預測：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%208.png"></p>
<ul>
<li>要使 RAS 能夠正確工作，需要符合下面兩個條件：
<ol>
<li>當遇到 call 指令時，需要能夠將 call 指令的 return address 寫進 RAS，而這需要識別出哪條指令是 call 指令
<ul>
<li>正常狀態下，只有到了 pipeline 的 decode stage 才能得知指令是否為 call 指令</li>
<li>但是現代處理器都需要好幾個 cycles 才能從 I-Cache 中取得指令，因此當指令達到 pipeline 的 decode stage 時，在這條指令後面已經有很多條指令進到 pipeline 內了，如果當中包含 return 指令 (e.g. 假設函式很短)，那麼這個 return 指令將無法從 RAS 中取得正確的 return address，造成分支預測失敗，降低處理器的執行效率</li>
<li>如果能在分支預測階段 (e.g. <a href="../superscalar-overview-ch4-part1/#41---%E6%A6%82%E8%BF%B0">fetch stage</a>) 就可以得知一條指令是否為 call 指令，也就是透過 PC 值就可以知道是否為 call 指令，那麼就可以即時將 call 指令的 return address 寫進 RAS；即使 call 指令後面馬上接著一條 return 指令，這條 return 指令也可以透過 RAS 來預測正確的目標位址</li>
<li>要實現這個功能依舊需要借助 BTB，BTB 中保存了所有發生跳轉的指令，且 call 和 return 指令總是會跳轉的，因此它們都會被保存在 BTB 中
<ul>
<li>
<p>需要在 BTB 中新增一個欄位，用來標記分支指令的類型</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%209.png"></p>
</li>
<li>
<p>而後只需要透過查詢 BTB，就可以得知這條分支指令是否為一個 call / return 指令了</p>
</li>
</ul>
</li>
</ul>
</li>
<li>當預測 return 指令的目標位址時，需要能夠選擇 RAS 中的輸出作為目標位址，而不是選擇 BTB 的輸出值，因此仍需要在分支指令預測階段 (e.g. fetch stage) 就知道指令的類型
<ul>
<li>使用 1. 的方法，透過 BTB 中新增的指令類型欄位，得知該分支指令是否是一個 recall 指令</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
<li>
<p>但如果一個 recursive 函式有很多層，導致 RAS 的空間不夠存放所有 retrun 指令的目標位址時，要如何處理?</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2010.png"></p>
<ul>
<li>
<p>當 call SHR 的時候，RAS 已經沒有空間可以存放其 return address 了：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2011.png"></p>
</li>
</ul>
</li>
<li>
<p>有兩種方法可以處理這種情況：</p>
<ol>
<li>
<p>不對 call 指令進行處理：</p>
<ul>
<li>不修改 RAS，新的 call 指令的 return address 會被直接拋棄，這樣在下一次執行 return 指令時，就一定會發生 mis-prediction</li>
<li>除此之外，這樣的作法還要求 RAS 的 pointer 不能改變，否則後續的 return 指令都無法對應到自己的目標位址</li>
<li>顯然這是一個<strong>較差</strong>的作法</li>
</ul>
</li>
<li>
<p>繼續按照順序將 call 指令的 return address 寫入 RAS，最舊的 return address 會從 RAS 中被 evicted：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2012.png"></p>
<ul>
<li>
<p>這樣的方式，當要執行被 evicted 的 return address 對應的 return 指令時，勢必一定會發生 mis-prediction，但這樣的方式比 1. 要來得好的原因在其存在一定的正確性：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2013.png"></p>
<ul>
<li>
<p>針對這樣的 recursive function，其實寫入到 RAS 和被 evicted 的 return address 都是同樣的：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2014.png"></p>
</li>
<li>
<p>這樣即使較舊的 return address 被 evicted，RAS 還是有可能給出正確的 return address</p>
<ul>
<li>P.S. 這邊假設 RAS 應該是一個 circular buffer</li>
</ul>
</li>
</ul>
</li>
<li>
<p>不過像這樣 RAS 都被同一個 call 指令的 return address 所佔用，是非常浪費空間的；對於連續執行的同一條 call 指令來說，其 return address 可以只保留一份，並透過新增一個 counter 來標記 call 指令被執行的次數</p>
<ul>
<li>E.g. 使用一個 8 bits counter，就可以紀錄到最多 256 次的 recursive call 了</li>
</ul>
</li>
<li>
<p>RAS 的 pointer 只有在執行 return 指令，counter 值減 1 後為 0 時，才會將 RAS 的 pointer 指向下一個 return address</p>
</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
<li>其他預測方法：
<ul>
<li>
<p>對於其他 call 和 return 指令以外的間接跳轉類型的分支指令，雖然其目標位址理論上可以有很多種可能性，不過實際上只會有幾個固定的位址而已，例如：</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><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-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-2">2</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-0-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-3">3</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-0-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-4">4</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-0-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-5">5</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-0-6"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-6">6</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-0-7"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-7">7</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-c" data-lang="c"><span style="display:flex;"><span><span style="color:#66d9ef">switch</span> (a) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">1</span><span style="color:#f92672">:</span> <span style="color:#75715e">/* Jump to address 1 */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">2</span><span style="color:#f92672">:</span> <span style="color:#75715e">/* Jump to address 2 */</span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">3</span><span style="color:#f92672">:</span> <span style="color:#75715e">/* Jump to address 3 */</span>
</span></span><span style="display:flex;"><span>    ...
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">case</span> <span style="color:#ae81ff">9</span><span style="color:#f92672">:</span> <span style="color:#75715e">/* Jump to address 9 */</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>以這個例子為例，目標位址最多就 9 種而已 (address 1 ~ address 9)，也就有機會預測其目標位址</li>
</ul>
</li>
<li>
<p>對於間接跳轉類型的分支指令，其目標位址也可能是和過去的執行情況有關，因此可以利用基於局部歷史預測的分支預測方法中的 BHR 對目標位址進行預測：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2015.png"></p>
<ul>
<li>跟之前 <code>2-bit saturating counter</code> 分支預測方法唯一的差別在於將 PHT 換成了 <code>Target Cache</code></li>
</ul>
</li>
</ul>
</li>
</ol>
<ul>
<li>總結：
<ul>
<li>使用 <strong>BTB</strong> 對<strong>直接跳轉類型的分支指令</strong>，和 <strong>call 指令</strong>進行預測</li>
<li>使用 <strong>RAS</strong> 對 <strong>return 指令</strong>進行預測</li>
<li>使用 <strong>BHR</strong> + <strong>Target Cache</strong> 對<strong>其他間接跳轉類型的分支指令</strong>進行預測</li>
</ul>
</li>
<li>尤其是 <strong>BTB</strong> 和 <strong>RAS</strong>，幾乎是現代 superscalar CPU 必備的元件</li>
</ul>
<h2 id="433---小結">4.3.3 - 小結</h2>
<ul>
<li>
<p>預測分支指令的<strong>方向</strong>：使用 <strong>BHR (多個 BHRs 組成 BHT)</strong>、<strong>GHR</strong> 和 <strong>PHT</strong> (<code>2-bit saturating counter</code>)</p>
</li>
<li>
<p>預測分支指令的<strong>目標位址</strong>：使用 <strong>BTB</strong>、<strong>RAS</strong> 和 <strong>BHR</strong> + <strong>Target Cache</strong></p>
</li>
<li>
<p>綜合到目前為止，完整的分支預測方法如下：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2016.png"></p>
</li>
<li>
<p>在 superscalar CPU 中，有很多階段都可以得到分支指令的結果，例如：</p>
<ul>
<li><strong>Fetch stage</strong> 可以得到直接跳轉分支指令的結果</li>
<li><strong>Execution stage</strong> 可以得到間接跳轉分支指令的結果</li>
</ul>
</li>
<li>
<p>在得到分支指令結果後，就可以跟分支預測是否正確做檢查，如果發生 mis-prediction，就需要將在這條分支指令之後 fetch 進 pipeline 的指令都給 flush 掉，也就會造成多個 bubbles，降低處理器的性能，i.e. mis-prediction penalty</p>
</li>
<li>
<p>此外，這些要被 flushed 的指令有可能已經更改了 pipeline 中的元件的內容 (e.g. register renaming 的 mapping table 被更新)，因此需要一個機制來恢復被 mis-prediction 更改的內容</p>
</li>
</ul>
<h1 id="44---分支預測失敗時的恢復">4.4 - 分支預測失敗時的恢復</h1>
<ul>
<li>
<p>在 pipeline 中，有很多 stages 可以對分支預測<strong>是否正確</strong>進行檢查：</p>
<ul>
<li>Decode stage：
<ul>
<li>可以得到一個 PC 值對應的指令是否為分支指令，以及這條分支指令的類型</li>
<li>如果是直接跳轉 (direct) 類型的指令，還可以在這個階段得到其目標位址
<ul>
<li>例如：RISC-V 的 <code>jal</code> 指令，在 decode stage 就可以由 offset 得知其目標位址，如果這條指令是預測 not taken，那就代表發生 mis-prediction</li>
</ul>
</li>
<li>在 decode stage 發現預測失敗的 mis-prediction penalty 最小，可以馬上進行恢復</li>
<li>然而，由於間接跳轉 (indirect) 類型指令的目標位址是存在暫存器中，在 decode stage 仍無法得知其目標位址，因此如果發生 mis-prediction，CPU 也沒辦法在這個 stage 取得下一個 fetch 指令的位址
<ul>
<li><strong>只能暫停 instruction fetch</strong></li>
</ul>
</li>
</ul>
</li>
<li>讀取暫存器的 stage (有可能是在 register renaming stage 之後，也有可能在 issue stage 之後，取決於 CPU 的設計)：
<ul>
<li>如果此時讀到了暫存器的值，那麼就可以對間接跳轉的分支指令的目標位址進行檢查，如果發現目標位址預測錯誤，那麼就可以用正確的目標位址來 fetch instruction
<ul>
<li>此時仍需在將該分支指令之後進入到 pipeline stage 的指令給 flushed 掉
<ul>
<li>但這並不容易實現，因為這些指令有可能已經進到 Issue queue 中了；Issue queue 中也包含了分支指令之前的指令，這些指令不應該被 flushed，因此需要<strong>有條件的 flush 在 Issue queue 中的指令</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>Execution stage：
<ul>
<li>
<p>不管是什麼類型的分支指令都可以在這個 execution stage 取得其正確的目標位址，可以對分支指令是否正確 (包含方向和目標位址) 進行檢查</p>
</li>
<li>
<p>但此時發生 mis-prediction 的 penalty 也是最大的</p>
</li>
<li>
<p>當發生 mis-prediction 時，所有在此分支指令之後的指令都要被 flushed 掉</p>
<ul>
<li>然而，針對 out-of-order CPU，在此分支指令之前的指令，有可能在 execution stage，也有可能還在 Issue queue 中，這些指令不應該被 flushed</li>
</ul>
</li>
<li>
<p>指令之間的順序是被記錄在 <strong>ROB (Re-Order Buffer)</strong> 中的，因此可以透過 ROB 來恢復發生 mis-prediction 時 CPU 的狀態</p>
</li>
<li>
<p>當發生 mis-prediction 時，將此訊息記錄在此分支指令對應的 ROB entry 中，並<strong>暫停 instruction fetch</strong>；<strong>當這條分支指令在 pipeline 中變成最老的指令時，就代表在 pipeline 中，剩餘的指令都比這條分支指令還新，可以直接將整個 pipeline stage 中的指令都 flush 掉，並從正確的目標位址 fetch instruction</strong></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2017.png"></p>
<ul>
<li>缺點：
<ul>
<li>需要分支指令在 pipeline 中一定時間後才能進行處理</li>
<li>如果在此分支指令之前有一道 D-cache miss 的 load 指令，這個等待的時間就會變得非常長，i.e. mis-prediction 的 penalty 很大，降低了處理器的性能
<ul>
<li>這段時間內都沒辦法 fetch 新的指令來執行</li>
</ul>
</li>
</ul>
</li>
<li>優點：
<ul>
<li>這種方法容易實現，是在硬體複雜度和執行效率之間一個比較折衷的方案</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>除了基於 ROB 的方法來恢復 mis-prediction 的狀態，現在很多處理器都採用了基於 <strong>Checkpoint</strong> 的方法來恢復 mis-prediction 的狀態</p>
<ul>
<li>在分支指令之後的指令更改 CPU 的狀態前，先將 CPU 的狀態保存下來，包含：
<ul>
<li>Register renaming 的 mapping table</li>
<li>預測 taken 的分支指令其對應的下一條指令的 PC… etc</li>
</ul>
</li>
</ul>
</li>
<li>
<p>通常在分支指令進入到 register renaming stage 時，就需要保存 CPU 的狀態</p>
</li>
<li>
<p>相比於使用 ROB 的方式，使用 Checkpoint 的方式需要消耗更多的硬體資源，但是這種方式可以快速地將 CPU 的狀態給恢復，並馬上從正確的 PC 值開始 fetch instruction，因此執行效率更高</p>
</li>
<li>
<p>當發生 mis-prediction 時，只有在該分支指令之後的指令才能被 flushed，在分支指令之前的指令要保留下來，因此需要一個機制來正確地識別哪些指令處在 mis-prediction 的路徑上</p>
<ul>
<li>
<p>可以透過對每一條分支指令都分配一個編號，所有在分支指令之後的指令都會給予同一個編號，直到碰到下一個分支指令為止；該編號會被記錄在 ROB entry 中</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2018.png"></p>
<ul>
<li>當發生 mis-prediction 時，就可以利用分支指令所分配到的編號，將所有在該分支指令之後的指令都 flushed 掉，而不需要等到分支指令變成 pipeline 中最老的指令</li>
</ul>
</li>
</ul>
</li>
<li>
<p>分支指令的編號個數決定了最多可以存在 pipeline 中分支指令的個數</p>
<ul>
<li>例如：假設一 CPU 支援最多 128 條指令在流水線中，假設每 5 條指令就有 1 個分支指令，最多會有 128 / 5 = 26 條分支指令存在 pipeline 中，也就是說需要 26 個編號，因此需要 5 個 bits (2^5 = 32) 來存放編號</li>
</ul>
</li>
<li>
<p>這些編號值會被保存在一個 FIFO (<strong>tag list</strong>) 中，一旦這個 FIFO 滿了，就沒辦法再向 pipeline 中送入分支指令</p>
<ul>
<li>當 tag list 滿時，如果又 decode 出一條分支指令，就得暫停 decode stage 之前的 pipeline，直到 tag list 有空間為止</li>
<li>Tag list 中的 entries 是 in-order 的</li>
</ul>
</li>
<li>
<p>其中一個設計方法：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2019.png"></p>
<ul>
<li>Free tag list：用來存放還沒有被分配的編號</li>
<li>Tag list：用來存放已經被分配的編號</li>
<li>在 decode stage 解析出一條新的分支指令時，就從 free tag list 中取得一個編號，指定給該分支指令，並編號加入 tag list 中</li>
<li>當發生 mis-prediction 時，就可以透過這個編號來識別所有在分支指令後面的指令，並從 pipeline 中給 flushed，最後再將這些編號加回 free tag list 中</li>
</ul>
</li>
<li>
<p>對於 superscalar CPU 而言，當一條分支指令發生 mis-prediction 時並需將在其之後的指令都從 pipeline 中給 flush 掉，實際上包含了以下兩個部份：</p>
<ul>
<li>在 issue stage 前的指令：
<ul>
<li>在 issue stage 前，指令都還是 in-order 的，因此當在 issue stage 中發現一條 mis-prediction 的分支指令時，只需將直接將在 issue stage 前的指令給 flush 掉即可
<ul>
<li>可以在 1 個 cycle 內完成</li>
</ul>
</li>
</ul>
</li>
<li>在 issue stage 及 issue stage 之後的指令：
<ul>
<li>從 issue stage 開始，指令就是 out-of-order 了，一部分的指令可能是在 mis-prediction 的路徑上的，因此需要透過上述的 tag list 的方式，找出在 mis-prediction 分支指令之後的指令並從 pipeline 中給 flush 掉
<ul>
<li>在 1 個 cycle 內無法完成</li>
</ul>
</li>
<li>Flush ROB 中的指令：
<ul>
<li>
<p>ROB 中有每個指令的編號，因此可以透過 tag list 找出 mis-prediction 分支指令之後所有指令的編號，並透過 broadcast 的方式通知 ROB，並將對應的 ROB entry 給 invalidate：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2020.png"></p>
<ul>
<li>這無法在 1 個 cycle 內完成，因為如果想要在 1 個 cycle 內比較所有的 ROB entries (最慘的情況下)，會需要非常大的硬體面積和增加 latency</li>
</ul>
</li>
</ul>
</li>
<li>Flush Issue queue 中的指令：
<ul>
<li>可以採用類似 ROB 的方式，將 Issue queue 中相關的指令給 flush 掉</li>
</ul>
</li>
<li>Flush issue stage 之後的指令：
<ul>
<li>可以採用類似 ROB 的方式，將 issue stage 之後的指令給 flush 掉</li>
</ul>
</li>
<li>其實並不一定要在 1 個 cycle 內將相關指令給 flushed 掉，因為在 execution stage 發現 mis-prediction 後並開始從正確的位址開始 fetch instructions，這些新指令需要經過好幾個 stages 才會進到 dispatch stage (到了 dispatch stage 才需要使用到 ROB 和 Issue queue)，因此只要在這些新的指令進到 dispatch stage 前來得及將相關的指令給 flush 掉即可，就不需要 stall pipeline 了</li>
<li>因此，一個折衷的方式就是每個 cycle 只從 tag list 中 broadcast 一個或幾個編號，這樣當編號個數不是很多時 (大部分情況是如此)，使用幾個 cycles 就可以將相關的指令給 flush 掉，也就不用 stall pipeline</li>
<li>只有在少數的情況下，當編號個數很多時，才需要花費多個 cycles 來 flush 相關的指令
<ul>
<li>新的指令在進入 issue stage 前，必須等待所有在 mis-prediction 路徑上的指令都被 flushed 並恢復 CPU 狀態後，才可以進入 issue stage</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>在把 pipeline 中相關的指令給 flush 掉後，就可以使用 Checkpoint 的方式回覆 CPU 的狀態</p>
<ul>
<li>主要是將 register renaming 的 mapping table 給恢復</li>
</ul>
</li>
<li>
<p>什麼時候對分支指令分配一個編號呢?</p>
<ul>
<li>Fetch stage：
<ul>
<li>Fetch stage 只有 PC 值，對於一個指令是否為分支指令只能用推測的方式，因此此時分配編號並不合適</li>
</ul>
</li>
<li>Decode stage：
<ul>
<li>只有在 decode stage 才可以真正判斷一條指令是否為分支指令，此時就可以對分支指令分配一個編號</li>
</ul>
</li>
</ul>
</li>
<li>
<p>由於 superscalar 一個 cycle 內會解析多個分支指令，因此最差的情況就是 N-ways 的 CPU，會有 N 條分支指令，也就代表 free tag list 和 tag list 都需要在一個 cycle 內提供 N 個編號，也就需要使用 multi-port FIFO 的設計，對硬體面積和功耗會有所影響</p>
<ul>
<li>實際上，絕大部分情況下，每個 cycle decode 的指令最多只會有一、兩條分支指令，如果使用 multi-port FIFO 的設計，大部分的 ports 都會是閒置狀態的，因此實際上並不會採用 multi-port FIFO 的設計</li>
<li>一種折衷的作法是限制每個 cycle 最多只能處理一條分支指令
<ul>
<li>如果在 decode stage 讀出來的 N 條指令中，有超過一條的分支指令，那麼只會從 free tag list 拿出一個編號，第二條以後的分支指令都必須 stall 到下一個 cycle 才能被處理</li>
<li>對大部分的程式來說分支指令都並不密集，因此這種方法不會對性能造成太大的負面影響</li>
</ul>
</li>
</ul>
</li>
<li>
<p>在 execution stage 檢查分支指令預測正確性時，要如何得知該分支指令之前所預測的值?</p>
<ul>
<li>
<p>可以將每條分支指令的預測值，存在 <code>PTAB (Prediction Target Address Buffer)</code> 中，且此分支指令在 PTAB 的 index 會隨著分支指令在 pipeline 中流動，等到分支指令進入 execution stage 時就可以用來跟實際的跳轉結果做比較</p>
<ul>
<li>在實際的處理器中，可以優化為<strong>只存預測為 taken 的分支指令</strong>預測值即可</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2021.png"></p>
</li>
<li>
<p>分支指令預測值與實際的結果有以下的可能性：</p>
<ul>
<li>實際結果為 <strong>not taken</strong>，並在 PTAB 中沒有找到對應的內容 (i.e. <strong>not taken</strong>) ⇒ 預測正確</li>
<li>實際結果為 <strong>not taken</strong>，但在 PTAB 中有找到對應的內容 (i.e. <strong>taken</strong>) ⇒ 預測錯誤，需要使用 <strong>Next PC</strong> 來作為正確的目標位址</li>
<li>實際結果為 <strong>taken</strong>，但在 PTAB 中沒有找到對應的內容 (i.e. <strong>not taken</strong>) ⇒ 預測錯誤，需要使用<strong>實際的目標位址</strong></li>
<li>實際結果為 <strong>taken</strong>，但在 PTAB 中有找到對應的內容 (i.e. <strong>taken</strong>) ⇒ 預測方向正確，但此時仍需要比較 predict address 是否與實際的目標位址相符：
<ul>
<li>相符：預測正確</li>
<li>不相符：需使用<strong>實際的目標位址</strong></li>
</ul>
</li>
</ul>
</li>
<li>
<p>寫 PTAB 的過程可以在 <strong>fetch stage</strong> 完成，只要在 <a href="../superscalar-overview-ch4-part1/#41---%E6%A6%82%E8%BF%B0">fetch stage 預測到</a>一條發生跳轉的分支指令，就可以將其寫進 PTAB 中</p>
</li>
</ul>
</li>
</ul>
<h1 id="45---超標量處理器的分支預測">4.5 - 超標量處理器的分支預測</h1>
<ul>
<li>對於 superscalar CPU，在 fetch stage 給一個 PC 值，可以透過 I-Cache 同時取出多條的指令並組成 fetch group，CPU 亦會根據 fetch group 中的指令個數，調整下一個 cycle instruction fetch 的位址，因此 superscalar CPU 的 instruction fetch PC 值並不會是連續的，且每次送到 I-Cache 的 fetch address 也只會是 fetch group 中第一條指令的位址而已
<ul>
<li>因此，如果只是根據第一條指令的位址做分支預測是不夠的</li>
</ul>
</li>
<li>如果對 instruction fetch 的位址進行限制，例如針對一個 4-way issue 的 CPU，每個 cycle fetch 的 instructions 都是 4-word aligned (共 4 條指令) 的，那麼就可以使用 fetch group 中所有指令共同的部份：<code>PC[31:4]</code> 來做 branch predict；對於 branch predictor 而言，它也只需要記下每個 4-word aligned 的指令中，第一條預測跳轉分支指令資訊即可
<ul>
<li>
<p>在大多數的情況下，4-word aligned 的指令只會有一條分支指令</p>
</li>
<li>
<p>不過仍需在 BTB 中記錄下該分支指令在這 4 條指令中的 index：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2022.png"></p>
<ul>
<li>此範例中，分支指令的 index 在這四條指令的 index 為 <code>01</code>，但此 cycle 的 instruction fetch 的 PC 值在 <code>10</code>，因此在進行分支預測時，由於分支指令並不在 instruction fetch 的範圍內，因此不會使用其預測值</li>
</ul>
</li>
<li>
<p>如果這 4-word aligned 的指令中，包含超過一條分支指令，那麼這些分支指令就會忽相干擾，影響分支預測的準確度，不過在大部分的程式中，分支指令出現的頻率並不會這麼高</p>
</li>
</ul>
</li>
<li>但實際上，每個 cycle fetch 的 instructions 是可以不侷限於 N-word aligned 的 (參考：<a href="../superscalar-overview-ch2/#24---%E8%B6%85%E6%A8%99%E9%87%8F%E8%99%95%E7%90%86%E5%99%A8%E7%9A%84%E5%8F%96%E6%8C%87%E4%BB%A4">2.4 - 超標量處理器的取指令</a>)
<ul>
<li>如一個 4-way issue 的 superscalar CPU，其每個 cycle fetch 的 instructions 是可以不侷限於 4-word aligned 的範圍內的，那麼要達到最理想的結果，就需要對一個 cycle 內 fetch group 內的指令都進行分支預測，如果當中存在 taken 的分支指令，那麼變需要並將第一個預測為 taken 的分支指令目標位址作為下一個 cycle 的 PC 值</li>
<li>實務上，要在一個 cycle 對 fetch group 內的指令都進行分支預測是不可接受的，因為這代表需要 BTB 支援 multi-port (參考：<a href="../superscalar-overview-ch2/#231---true-multi-port">2.3.1 - True Multi-port</a>)，才能在一個 cycle 內同時讀出多條指令的目標位址；即使採用 interleaving (參考：<a href="../superscalar-overview-ch2/#233---multi-banking">2.3.3 - Multi-banking</a> ) 的方式來避免使用 multi-port，但考慮到在使用過程中，最多只會使用一個 port 的值 (i.e. 第一個預測為 taken 的分支指令)，所以這種方式對硬體使用效率是非常低的</li>
</ul>
</li>
<li>如果可以在分支指令的方向預測完畢後，再進行目標位址的預測，就可以避免 multi-port BTB 使用的需求
<ul>
<li>但是這樣變成分支方向和目標位址的預測是 serialized 的，有可能就沒辦法在一個 cycle 內完成，也就無法”連續地”預測每個 cycle 的 instruction fetch 的位址</li>
</ul>
</li>
<li>不過大部分的分支指令都是直接跳轉類型的，其分支指令的目標位址是可以很快地被計算出來；對於此類的指令，其實並不需要進行目標位址的預測，而是直接在 instruction fetch 的階段，就馬上進行目標位址的計算
<ul>
<li>雖然不一定能在一個 cycle 內完成分支預測 (還有分支方向需預測)，但不一定會比 serialized 的方式來得慢，而且還可以取得分支指令正確的目標位址</li>
<li>要實現此功能，必須在指令進入 I-Cache 之前，就先進行 pre-decode，並馬上識別出分支指令，從而進行快速的分支位址的計算</li>
<li>但對於間接跳轉類型的指令 (除了 return 指令外)，就無法採用此方式了</li>
</ul>
</li>
<li>對於 superscalar CPU，由於需要一個 cycle 內對多條指令進行方向預測，從而找到第一條 taken 的分支指令：
<ul>
<li>基於局部歷史的分支預測：
<ul>
<li>需要 PHT 和 BHT 都支援 multi-port，可以透過 interleaving 的方式來避免 multi-port 的設計 (參考：<a href="../superscalar-overview-ch2/#233---multi-banking">2.3.3 - Multi-banking</a> )
<ul>
<li>
<p>例如將 PHT 改成 interleaving：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part2/image%2023.png"></p>
<ul>
<li><code>Addr[1:0]</code> 用來作為 multi-bank 的 index</li>
<li><code>Addr[6:2]</code> 用來作為 bank 中 (sub-)PHT 的 index</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>基於全局歷史的分支預測：
<ul>
<li>並無法簡單地將 PHT 改成 multi-port 即可，因為多條指令對應的 GHT 有可能是不同的，需要進行特殊的處理</li>
</ul>
</li>
</ul>
</li>
<li>Interleaving 在 superscalar CPU 中是經常被使用的，大部分需要 multi-port 設計的元件，如 ROB、Issue Queue、Instruction Buffer… etc，都可以採用 interleaving 的方式來設計</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>&lt;超標量處理器概覽&gt; 第 4 章 - 分支預測 (Part 1)</title>
      <link>https://0xc0de.xyz/posts/superscalar-overview-ch4-part1/</link>
      <pubDate>Fri, 28 Feb 2025 00:00:00 +0800</pubDate>
      <guid>https://0xc0de.xyz/posts/superscalar-overview-ch4-part1/</guid>
      <description>&lt;h1 id=&#34;41---概述&#34;&gt;4.1 - 概述&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;分支預測和 Cache 左右著 CPU 的效能，對於 superscalar CPU 來說，準確度高的分支預測更為重要&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在一般的 RISC 指令集中，分支指令包含兩個要素：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;方向：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;分支指令的方向只有兩種：跳轉 (taken)，或是不跳轉 (not taken)&lt;/li&gt;
&lt;li&gt;有些跳轉指令是無條件執行的 (e.g. &lt;code&gt;jmp&lt;/code&gt; 指令)，有些跳轉指令則是需要根據指令中攜帶的條件是否成立來決定是否發生跳轉 (e.g. &lt;code&gt;beq&lt;/code&gt; 指令)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;目標位址：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;對於一般的 RISC 指令集，目標位址在指令中有兩種形式：
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;直接跳轉 (direct)：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;目標位址 = 當前分支指令 (或分支指令的下一條指令) 的 PC 值 + immediate PC offset&lt;/li&gt;
&lt;li&gt;由於指令通常只有 32/64 bits，因此 immediate PC offset 的值範圍也就有限，因此也限制了直接跳轉能夠跳轉的範圍&lt;/li&gt;
&lt;li&gt;因為 immediate PC offset 值是 encode 在分支指令中，因此在 pipeline 的 &lt;strong&gt;decode stage&lt;/strong&gt; 就可以直接將 immediate PC offset 給解析出來，因此這種類型的指令是很容易進行分支預測的&lt;/li&gt;
&lt;li&gt;這種跳轉指令的執行效能也比較好&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;間接跳轉 (indirect)：&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;目標位址是來自於 register 的內容，由於 register 是 32/64 bits，因此可以跳轉到任何位址&lt;/li&gt;
&lt;li&gt;由於跳轉位址來自於 register，因此此類的分支指令的目標位址需要等待一段時間才能獲得 (e.g. 要等到 pipeline 的 &lt;strong&gt;execute stage&lt;/strong&gt;)
&lt;ul&gt;
&lt;li&gt;在確認目標位址前進到 pipeline 的指令，有可能都會是無效的 (if taken)，因此增大了 mis-prediction penalty&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;且由於 register 的內容是會動態變化的，因此這類的分支指令很難對目標位址進行預測
&lt;ul&gt;
&lt;li&gt;不過慶幸的是，這類指令通常都是 call 或是 return 類型的指令，這類指令都有很強的規律性，是容易被預測的&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;MIPS R3000 只使用了 5 級的 pipeline，在第二個階段的 decode stage，就可以得到分支指令跳轉的目標位址，即使發生了跳轉 (taken)，也只需要丟棄一道仍在 fetch stage 的指令，misprediction penalty 並不高&lt;/p&gt;</description>
      <content:encoded><![CDATA[<h1 id="41---概述">4.1 - 概述</h1>
<ul>
<li>
<p>分支預測和 Cache 左右著 CPU 的效能，對於 superscalar CPU 來說，準確度高的分支預測更為重要</p>
</li>
<li>
<p>在一般的 RISC 指令集中，分支指令包含兩個要素：</p>
<ol>
<li><strong>方向：</strong>
<ul>
<li>分支指令的方向只有兩種：跳轉 (taken)，或是不跳轉 (not taken)</li>
<li>有些跳轉指令是無條件執行的 (e.g. <code>jmp</code> 指令)，有些跳轉指令則是需要根據指令中攜帶的條件是否成立來決定是否發生跳轉 (e.g. <code>beq</code> 指令)</li>
</ul>
</li>
<li><strong>目標位址：</strong>
<ul>
<li>對於一般的 RISC 指令集，目標位址在指令中有兩種形式：
<ul>
<li><strong>直接跳轉 (direct)：</strong>
<ul>
<li>目標位址 = 當前分支指令 (或分支指令的下一條指令) 的 PC 值 + immediate PC offset</li>
<li>由於指令通常只有 32/64 bits，因此 immediate PC offset 的值範圍也就有限，因此也限制了直接跳轉能夠跳轉的範圍</li>
<li>因為 immediate PC offset 值是 encode 在分支指令中，因此在 pipeline 的 <strong>decode stage</strong> 就可以直接將 immediate PC offset 給解析出來，因此這種類型的指令是很容易進行分支預測的</li>
<li>這種跳轉指令的執行效能也比較好</li>
</ul>
</li>
<li><strong>間接跳轉 (indirect)：</strong>
<ul>
<li>目標位址是來自於 register 的內容，由於 register 是 32/64 bits，因此可以跳轉到任何位址</li>
<li>由於跳轉位址來自於 register，因此此類的分支指令的目標位址需要等待一段時間才能獲得 (e.g. 要等到 pipeline 的 <strong>execute stage</strong>)
<ul>
<li>在確認目標位址前進到 pipeline 的指令，有可能都會是無效的 (if taken)，因此增大了 mis-prediction penalty</li>
</ul>
</li>
<li>且由於 register 的內容是會動態變化的，因此這類的分支指令很難對目標位址進行預測
<ul>
<li>不過慶幸的是，這類指令通常都是 call 或是 return 類型的指令，這類指令都有很強的規律性，是容易被預測的</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
<li>
<p>MIPS R3000 只使用了 5 級的 pipeline，在第二個階段的 decode stage，就可以得到分支指令跳轉的目標位址，即使發生了跳轉 (taken)，也只需要丟棄一道仍在 fetch stage 的指令，misprediction penalty 並不高</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image.png"></p>
<ul>
<li>MIPS CPU 甚至為了減少這一個 cycle 的浪費，還會特別找一條一定會被執行，不相關的指令放在分支指令之後；不論分支是否跳轉或不跳轉，該指令都一定會被執行
<ul>
<li>分支指令後面的那個位置就被 MIPS 稱為分支延遲槽 (branch delay slot)，需要透過 compiler 或 programmer 找到一條不相關一定會被執行的指令，將其放置分支延遲槽中</li>
<li>但當 CPU 的並行度提高，以及 pipeline 的加深，當發生 misprediction 的時候，pipeline 中已經有很多指令都錯誤地進到 pipeline 裡了；這些指令都必須被拋棄，此時技使使用 branch delay slot，也很難得到滿意的結果，因為已經無法從分支指令之前的程序中找到這麼多不相關的指令了</li>
</ul>
</li>
</ul>
</li>
<li>
<p>對於 superscalar CPU，要進行分支預測，首先要從 I-Cache 取出的指令組 (<code>fetch group</code>) 中，判斷哪些指令是分支指令：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%201.png"></p>
<ul>
<li>
<p>最容易的辦法是將 <code>fetch group</code> 中的指令，從 I-Cache 取出後，進行快速的 decode</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%202.png"></p>
<ul>
<li>從 I-Cache 取出指令後，只需辨別目前的指令是否為一個分支指令，如果是的話，就可以直接將分支指令對應的 PC 值送到 branch predictor 中，就可以對分支指令進行預測了</li>
<li>然而，當 cycle time 比較短的時候，I-Cache 的訪問可能需要好幾個 cycles 才能完成 (e.g. 可能需要訪問 L2 Cache、memory)；在這些 cycles 內由於無法獲得準確的預測結果，只能照順序讀取指令 (i.e. not taken)，降低的分支預測的準確度，進而影響 CPU 的效能</li>
<li>此外，在指令從 I-Cache 讀出來後，fast decode 和 branch predict 是放在同一個 cycle 內的，會嚴重影響 CPU 的 cycle time</li>
<li>為了解決以上的問題，可以在指令從 L2 Cache 寫入 I-Cache 之前進行快速的 decode，也就是 <code>pre-decode</code>，然後將指令是否是分支指令的資訊和指令一起寫進 I-Cache 中；雖然這樣會增加 I-Cache 的面積，但可以省略 fast decode 的電路，在一定程度上緩解 對 CPU cycle time 的影響
<ul>
<li>但是 instruction fetch 到得到分支指令的預測結果這段的間隔時間還是過長的，無法透過以上的方式來解決</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>在 pipeline 中，分支預測越靠前面幾個 stages 是越好的，如果指令從 I-Cache 讀出後才進行分支預測，那麼由於 I-Cache 的訪問可能需要多於 1 個  cycle 才能完成，可能經過了好幾個 pipeline stages 了才能得到分支指令預測的結果，在此期間已經有很多的指令進入到了 pipeline，但如果發生 misprediction，這段期間進入到 pipeline 的指令都必須被 flushed，降低了CPU 的執行效能</p>
<ul>
<li>因此分支預測，最好是可以發生在一得到 instruction fetch 的 PC 值時，在 instruction fetch 的同時進行分支預測，這樣在下一個 cycle 的時候，根據分支預測的結果來 fetch instruction
<ul>
<li>在同一個 process 內，PC 的 VA 值都是固定的，因此可以直接拿 PC 值來進行預測</li>
<li>當發生 context-switch 時，需要將 branch predictor 的內容給清空，才能保證不同 processes 間的分支預測不會互相影響
<ul>
<li>如果有使用 ASID，那麼可以將 ASID 和 PC 值一起進行分支預測，如此在發生 context-switch 時，就不用清空 branch predictor</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>在一得到 instruction fetch 的 PC 值時，根據 PC 值來預測當下 cycle 的 instruction fetch 中是否存在分支指令，以及分支指令的方向和目標位址：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%203.png"></p>
</li>
<li>
<p>分支預測是影響 CPU 效能的關鍵因素之一，需要在硬體面積及功耗、預測準確度和 latency 之間找到一個平衡點</p>
</li>
</ul>
<h1 id="42---分支指令的方向預測">4.2 - 分支指令的方向預測</h1>
<ul>
<li>
<p>最簡單的動態預測方法就是直接使用上一次分支的結果 (last-outcome prediction)：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%204.png"></p>
<ul>
<li>
<p>相比於靜態分支預測，使用 last-outcome prediction 在不同的情況下可以獲得更優的結果；然而當分支指令發生變化時，其準確度有可能比靜態分支預測還差：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%205.png"></p>
<ul>
<li>當分支每次都發生變化時，此方法的分支預測失敗率是 ~100%</li>
</ul>
</li>
</ul>
</li>
<li>
<p>因此，last-outcome prediction 的準確度在現在處理器是無法被接受的，因此也沒有被現代處理器所採納</p>
</li>
<li>
<p>現代處理器用得最廣泛的分支預測方法，都是基於 <code>2-bit saturating counter</code> 為基礎而衍生出的不同實做</p>
</li>
</ul>
<h2 id="421---基於兩位飽和計數器的分支預測">4.2.1 - 基於兩位飽和計數器的分支預測</h2>
<ul>
<li>
<p><code>2-bit saturating counter</code> 分支預測方法不會馬上使用分支指令上一次的結果，而是根據一條分支指令前兩次的執行結果來預測本次分支的方向，通常有四種 FSM 狀態：</p>
<ul>
<li>
<p>Strongly taken (編碼：<code>11</code>)：Counter 處於飽和狀態，預測此次分支會發生跳轉 (taken)</p>
</li>
<li>
<p>Weakly taken (編碼：<code>10</code>)：Counter 不處於飽和狀態，預測此次分支會發生跳轉 (taken)</p>
</li>
<li>
<p>Weakly not taken (編碼：<code>01</code>)：Counter 不處於飽和狀態，預測只次分支不會發生跳轉 (not taken)</p>
</li>
<li>
<p>Strongly not taken (編碼：<code>00</code>)：Counter 處於飽和狀態，預測此次分支不會發生跳轉 (not taken)</p>
</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%206.png"></p>
<ul>
<li>當分支指令總是朝著一個方向的時候，FSM 就會處於飽和狀態，此時預測的正確率會比較高</li>
<li>當分支指令的方向總是發生變化時，FSM 無法處於飽和狀態，此時預測的準確率比較低</li>
</ul>
</li>
<li>
<p>其他 <code>2-bit saturating counter</code> 的實現方式：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%207.png"></p>
<ul>
<li>上：Weakly not taken 只要分支指令一跳轉 (taken)，就將 FSM 設為 Strongly taken</li>
<li>下：Weakly taken 只要分支指令一不跳轉 (not taken)，就將 FSM 設為 Strongly not taken</li>
<li>不過實務上，這樣的設計並不會讓 benchmark 結果變得比較好，因此這種兩實做並沒有很常使用</li>
</ul>
</li>
<li>
<p>一般來說，都是使用 Strongly not taken 或 Weakly not taken 作為 FSM 的起始狀態</p>
</li>
<li>
<p>可以使用 Gray code 將 FSM 的狀態編碼，保證在狀態轉換時，只有一個 bit 會發生變化</p>
</li>
<li>
<p>考慮以下 for-loop：</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><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-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-2">2</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-0-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-0-3">3</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-c" data-lang="c"><span style="display:flex;"><span><span style="color:#66d9ef">for</span> (i <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>; i <span style="color:#f92672">&lt;</span> m; i<span style="color:#f92672">++</span>) {
</span></span><span style="display:flex;"><span>    ...
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>假設初始狀態是在 Weakly not taken：
<ul>
<li><code>i = 0</code>：Weakly not taken ⇒ 預測<strong>失敗</strong>，FSM 切換到 Weakly taken</li>
<li><code>i = 1</code>：Weakly taken ⇒ 預測b，FSM 切換到 Strongly taken</li>
<li><code>i = 2</code> ~ <code>i = m - 1</code>：Strongly taken ⇒ 預測<strong>成功</strong>，FSM 保持在 Strongly taken</li>
<li><code>i = m</code>，離開迴圈：Strongly ⇒ 預測<strong>失敗</strong>，FSM 切換到 Weakly taken</li>
</ul>
</li>
<li>整個迴圈失敗的比例：<code>2 / m</code>，如果 m 很大，那麼這樣的預測結果就是可以接受的</li>
</ul>
</li>
<li>
<p>對於絕大數的程式來說，使用 <code>2-bit saturating counter</code> 就可以獲得比較高的準確率了；增加 counter 的 bits 數 (e.g. 使用 <code>3-bit saturating counter</code>)，會讓硬體的複雜度上升和更多的空間需求，這些額外的開銷所帶來的負面影響可能大於實際可以獲得預測準確度的提高，因此主流 CPU 都是使用 <code>2-bit saturating counter</code></p>
</li>
<li>
<p>分支預測都是基於 PC 值為基礎運行的，正常來說，每一個 PC 值都應該對應一個 2-bit 的 saturating counter，因此 32-bit CPU 就需要 <code>2^30 * 2</code> 個 bits (假設 PC 值都是 32 bits，所以最低 2 bits 不考慮)，顯然實務上不可能使用一個這麼大的硬體空間</p>
</li>
<li>
<p>由於不是所有指令都是分支指令，因此一般都使用以下的方法來紀錄 2-bit saturating counters：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%208.png"></p>
<ul>
<li><code>PHT (Pattern History Table)</code> 是一個表格，用來存放每個 <code>k-bit</code> PC 值所對應的 <code>2-bit saturating counters</code></li>
<li>這個 PHT 總共儲存了 <code>2^k</code> 個 saturating counters，因此 PHT 的大小為：<code>2^k * 2 bits</code>；PHT entry 是透過 <code>k-bit</code> 的 PC 來索引的</li>
<li>使用 <code>k-bit</code> PC 來索引 PHT，必然就導致了有相同 <code>k-bit</code> 的 PC 都會索引到同一個 saturating counter，如果這些有相同 <code>k-bit</code> 的 PC 對應到的指令不只一條是分支指令，那麼這些分支指令彼此間就會相互影響，也就是造成 <code>aliasing</code> 的問題
<ul>
<li>Neutral aliasing：多條 aliasing 的指令跳轉的方向都相同</li>
<li>Destructive aliasing：多條 aliasing 的指令跳轉方向不相同</li>
</ul>
</li>
<li>雖然這樣的實做方法會有 aliasing 的問題，但考量到這樣的方法實做起來比較簡單，且佔用的硬體空間也不大，因此輕微的預測準確度下降也是可以接受的</li>
</ul>
</li>
<li>
<p>PHT 的大小對分支預測準確度的影響：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%209.png"></p>
<ul>
<li>PHT 的大小是 2 KB，準確度已經達到 93% 以上
<ul>
<li>k = log(2 * 1024 * 8 bits) / 2 bits = 13，也就是使用 PC 中的 13 bits 來索引 PHT entry</li>
</ul>
</li>
</ul>
</li>
<li>
<p>避免 PHT aliasing 的方法：</p>
<ul>
<li>
<p>將 PC 做 hash 後，再拿 <code>k-bit</code> 的 hash 值去索引 PHT entry：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2010.png"></p>
</li>
<li>
<p>hash 的實做方法可以很簡單，例如使用普通的 XOR 運算，或是設計得更複雜以取得更好的 hash 結果</p>
</li>
</ul>
</li>
<li>
<p><code>2-bit saturating counters</code> 需要根據跳轉指令的結果，更新對應的 PHT entry，有三個時間點可以更新 PHT：</p>
<ol>
<li>在 Fetch stage，分支預測時，更新 PHT entry</li>
<li>在 Execution stage，分支指令的方向被實際運算出來時，更新 PHT entry</li>
<li>在 Commit stage，當分支指令要離開 pipeline 時，更新 PHT entry</li>
</ol>
</li>
<li>
<p>第一種方法，在 Fetch stage 根據分支預測的結果，直接更新 PHT entry，是否不可靠的，因為分支預測的結果有可能是錯誤的</p>
</li>
<li>
<p>第二、第三種方法，由於 superscalar CPU 的 pipeline stages 都很深，且每個 cycles 會執行多條指令，導致一條分支指令可能在 PHT entry 被更新前就被 fetched 過很多次了 (e.g. 很小的 for-loop body)，這條分支指令在進行分支預測時，就沒有利用到它之前被執行的分支結果</p>
<ul>
<li>但考量到 saturating counter 的特性，只要 saturating counter 處於飽和的狀態，其分支預測的結果就會比較固定，即使更新 PHT entry 的時間點較晚，也不會改變 saturating counter 的狀態，所以對分支預測準確度並不會造成太大的負面影響</li>
<li>此外，雖然第二種在 Execution stage 更新 PHT entry 比第三種在 Commit stage 才更新 PHT entry 的時間點早，但在 out-of-order CPU，即使在 Execution stage 得到分支指令的結果，也不能保證它一定會被執行 (有可能這個分支指令是在 mis-prediction 的路徑上)，有可能會被 flushed 掉，因此不能利用這個分支指令在 Execution stage 的結果來更新 PHT entry</li>
<li>所以，只有第三種在 Commit stage 更新 PHT entry 的方式，才是萬無一失的</li>
</ul>
</li>
<li>
<p>這種基於 <code>2-bit saturating counter</code>  的分支預測方法，其預測準確度有一定的上限，很難達到 98% 以上的準確度，因此現代處理器都<strong>不會直接</strong>使用此方法</p>
</li>
</ul>
<h2 id="422---基於局部歷史的分支預測">4.2.2 - 基於局部歷史的分支預測</h2>
<ul>
<li>
<p>針對以下的情境，<code>2-bit saturating counter</code> 並不會有比較高的準確度：</p>
<pre><code>  ![image.png](image%2011.png)
</code></pre>
<ul>
<li>針對這樣的情境，<code>2-bit saturating counter</code> 並沒辦法達到飽和的狀態，始終在 Weakly not taken 和 Weakly taken 之間做切換
<ul>
<li>假設 FSM 的起始狀態是 Weakly not taken，此時的分支預測準確率是 0%</li>
<li>假設 FSM 的起始狀態是 Strongly not taken，此時的分支預測準確率是 50%</li>
</ul>
</li>
<li>不論是哪一種情況，這樣的分支預測準確度都是無法接受的</li>
</ul>
</li>
<li>
<p>可以使用 <code>BHR (Branch History Register)</code> 來紀錄一條分支指令過去的歷史狀態，這種預測方法稱為<strong>基於局部歷史 (Local History)</strong> 的預測方法</p>
<ul>
<li>透過將每次分支的結果 (1: Taken，0: Not taken)，從右 shift 進 BHR，就可以紀錄這條分支指令的歷史狀態了</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2012.png"></p>
<ul>
<li>一個寬為 <code>n</code> 位的 BHR 可以紀錄一條分支指令過去 <code>n</code> 次的分支預測結果 (taken or not taken)，並使用 BHR 去索引 PHT 的 entry</li>
<li>PHT 仍是一個表格，大小為 <code>2^n * 2</code> bits，每個 PHT entry 都存儲了 saturating counter 的值
<ul>
<li>FSM Update Logic 硬體仍是獨立於 PHT 之外，每個 PHT entry 都是透過這個 FSM Update Logic 來更新其 counter 值</li>
</ul>
</li>
<li>當一條分支指令得到其分支結果時，就將對應 PHT entry 的 counter 值讀出來，並根據其分支結果進行更新，然後將結果寫回 PHT entry</li>
</ul>
</li>
<li>
<p>假設有一條分支指令，其 BHR 的寬度為 <strong>2 bits</strong>，也就是這條分支指令的<strong>前 2 次</strong>分支歷史結果會被紀錄在 BHR 中：</p>
<ul>
<li>
<p>BHR 的值總共四種可能：</p>
<ul>
<li><code>00</code>：此分支指令前兩次都是 not taken</li>
<li><code>01</code>：此分支指令前兩次分別是：not taken → taken</li>
<li><code>10</code>：此分支指令前兩次分別是：taken → not taken</li>
<li><code>11</code>：此分支指令前兩次都是 taken</li>
</ul>
</li>
<li>
<p>假設這條分支指令有如下的執行順序：taken → not taken → taken → not taken → taken…，則此 BHR 的值依執行順序分別會是 (從右 shift 進 BHR，BHR 的初始值為 <code>00</code>)：</p>
<ul>
<li>taken：<code>01</code></li>
<li>not taken：<code>10</code></li>
<li>taken：<code>01</code></li>
<li>not taken：<code>10</code>
<ul>
<li>可以統整出：
<ul>
<li>當 BHR 的值為 <code>01</code> 時，下次 shift 進 BHR 的值一定會是 <code>0</code>，i.e. 下次是 not taken</li>
<li>當 BHR 的值為 <code>10</code> 時，下次 shift 進 BHR 的值一定會是 <code>1</code>，i.e. 下次是 taken</li>
</ul>
</li>
<li>BHR 值：<code>10</code> → 對應到 PHT <strong>entry 2</strong>，此 PHT entry 的 saturating counter 會是 Strongly taken 的飽和狀態 (因為每次都是 taken)</li>
<li>BHR 值：<code>01</code> → 對應到 PHT <strong>entry 1</strong>，此 PHT entry 的 saturating counter 會是 Strong not taken 的飽和狀態 (因為每次都是 not taken)</li>
<li>BHR 值：<code>00</code> 和 <code>11</code>，沒有用到，因此 PHT <strong>entry 0</strong>，<strong>entry 3</strong> 也就不會被使用到，也就是 4 個 PHT entries 中，只有 2 個 entries 是有起作用的</li>
</ul>
</li>
</ul>
</li>
<li>
<p>假設執行順序是：TTNNTTNNTTNN&hellip;，BTN 的序列值會是：<code>110011001100</code></p>
<ul>
<li><code>11</code> 之後必接 <code>0</code></li>
<li><code>10</code> 之後必接 <code>0</code></li>
<li><code>00</code> 之後必接 <code>1</code></li>
<li><code>01</code> 之後必接 <code>1</code></li>
<li>也就是說，序列中每 2 位數後跟著的值都是唯一的，稱這個序列的循環週期為 2
<ul>
<li><strong>entry 3</strong>、<strong>entry 2</strong> 最終會是 Strongly not taken</li>
<li><strong>entry 0</strong>、<strong>entry 1</strong> 最終會是 Strongly taken</li>
</ul>
</li>
</ul>
</li>
<li>
<p>對於一個最小循環週期為 <code>n</code> 的序列來說，任何<code>大於 n</code> 的值都可以作為這個序列的循環週期</p>
<ul>
<li>例如：<code>110011001100</code>
<ul>
<li><code>n = 2</code>：
<ul>
<li><code>11</code> 之後必接 <code>0</code></li>
<li><code>10</code> 之後必接 <code>0</code></li>
<li><code>00</code> 之後必接 <code>1</code></li>
<li><code>01</code> 之後必接 <code>1</code></li>
</ul>
</li>
<li><code>n = 3</code>：
<ul>
<li><code>110</code> 之後必接 <code>0</code>，與 <code>n = 2</code>，<code>10</code> 相同</li>
<li><code>100</code> 之後必接 <code>1</code>，與 <code>n = 2</code>，<code>00</code> 相同</li>
<li><code>001</code> 之後必接 <code>1</code>，與 <code>n = 2</code>，<code>01</code> 相同</li>
<li><code>011</code> 之後必接 <code>0</code>，與 <code>n = 2</code>，<code>11</code> 相同</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>如果一個序列中，連續相同的 taken 或 not taken <strong>最多</strong>有 <code>p</code> 個，那麼這個序列的循環週期就為 <code>p</code></p>
</li>
<li>
<p>例如，假設執行順序是：TTTNTTTN&hellip;，BTN 的序列值會是：<code>11101110</code>，其循環週期為 3</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2013.png"></p>
</li>
</ul>
</li>
<li>
<p>更寬的 BNT：</p>
<ul>
<li>優點：
<ul>
<li>可以紀錄到分支指令越多的歷史分支結果，提高分支預測的準確度</li>
</ul>
</li>
<li>缺點：
<ul>
<li>要讓此 BNT 達到飽和，需要更長的訓練時間；在達到飽和狀態前，分支預測的準確度是比較低的</li>
<li>需要的 PHT entries 數也更多，佔用硬體空間</li>
</ul>
</li>
</ul>
</li>
<li>
<p>將所有分支指令的 BHR 組合起來稱作 <code>BHT (Branch History (Register) Table)</code></p>
</li>
<li>
<p>在實務中，BHT 不可能照顧到所有的 PC (一個 <code>n</code> 位的 BHR，需要的 PHT 空間為 <code>2^n * 2</code>)</p>
</li>
<li>
<p>一般都是採用 <code>k-bit</code> PC 來索引 BHT 中的 BHR，<code>t-bit</code> PC 來索引 PHT，再根據 BHR (寬度為 <code>n-bit</code>) 的值，索引 PHT entry：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2014.png"></p>
<ul>
<li>BHR = <code>n</code> bits</li>
<li>BHT = <code>2^k * n</code> bits</li>
<li>PHT = <code>2^n * 2</code> bits</li>
<li>PHTs = <code>(2^n * 2) * 2^t</code> bits</li>
<li>一般來說：<code>t &lt; k</code></li>
</ul>
</li>
<li>
<p>由於使用了 PC 的一部分來索引 BHT 和 PHTs，有可能會碰到 aliasing 的問題；這些 aliased 的分支指令會使用同一個 BHR 及 PHT，彼此之間會互相影響，使分支預測的準確度下降</p>
</li>
<li>
<p>以下兩種情況會造成 PHT aliasing：</p>
<ol>
<li>兩條分支指令的 <code>k-bit</code> PC 相同，因此索引到同一個 BHR，也就對應到同一個 PHT entry</li>
<li>兩條分支指令的 <code>k-bit</code> PC 不同，索引到不同的 BHR，但 BHR 的內容相同，因此對應到w同一個 PHT entry</li>
</ol>
</li>
<li>
<p>為了避免上述的兩種狀況，可以將 PC 值和對應的 BHR 值進行一定的處理，使用處理後的值來索引 PHT：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2015.png"></p>
<ul>
<li>將 BHR 的值和分支指令的 PC 值的一部分拼接，用得到的新的值索引 PHT 來得到分支預測結果
<ul>
<li>這種方法將分支指令的 PC 值也考慮進來，一定程度上避免了 PHT aliasing 的問題，因此可以提高分支預測的準確度</li>
</ul>
</li>
</ul>
</li>
<li>
<p>上述 BHR 的值和分支指令的 PC 值的拼接方法其實效果並不是很理想，還有其他的方式可以對 BHR 的值和分支指令的 PC 值做處理，獲得的效果會更好一點，例如使用 XOR：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2016.png"></p>
</li>
<li>
<p>現實處理器會使用更複雜的演算法，盡量避免 PHT aliasing 的情況發生</p>
</li>
<li>
<p>現實情況中，當一條分支指令的循環週期太大時 (e.g. 比較大的 for-loop body)，就需要一個寬度很大的 BHR，這會導致過長的訓練時間，並且 PHT 也會隨之佔用更大的硬體空間，這在現實處理器是無法接受的</p>
<ul>
<li>現實處理器只能用有限寬度的 BHR，寬度不夠，就會導致就算是很有規律的分支指令 (e.g. 999 次的 taken → 1 次的 not taken)，也無法獲得最完美的分支預測結果</li>
</ul>
</li>
<li>
<p>基於 <code>2-bit saturating counter</code> 的分支預測方法可以稱為<strong>基於局部歷史 (Local History) 的分支預測</strong>，因為都是根據同一條分支指令過去的執行結果來預測，並沒有考量到其他條分支指令的執行結果對預測結果造成的影響</p>
<ul>
<li>有時候一條分支指令的執行結果並不取決於自身在過去的執行結果，而是和它前面的分支指令的結果息息相關，因此需要採用<strong>基於全局歷史 (Global History) 的分支預測</strong>方法</li>
</ul>
</li>
</ul>
<h2 id="423---基於全局歷史的分支預測">4.2.3 - 基於全局歷史的分支預測</h2>
<ul>
<li>
<p>當對一條分支指令進行分支預測時，如果同時考慮到它前面分支指令的執行結果，則稱這種分支預測方法為：<strong>基於全局歷史 (Global History) 的分支預測</strong></p>
</li>
<li>
<p>一條分支指令什麼時候會和它前面的分支指令的執行結果相關呢?</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><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-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-3">3</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-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-4">4</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-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-5">5</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-6"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-6">6</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-7"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-1-7">7</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-c" data-lang="c"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (aa <span style="color:#f92672">==</span> <span style="color:#ae81ff">2</span>)    <span style="color:#75715e">// b1
</span></span></span><span style="display:flex;"><span>    aa <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (bb <span style="color:#f92672">==</span> <span style="color:#ae81ff">2</span>)    <span style="color:#75715e">// b2
</span></span></span><span style="display:flex;"><span>    bb <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (aa <span style="color:#f92672">!=</span> bb) { <span style="color:#75715e">// b3
</span></span></span><span style="display:flex;"><span>    ....
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>如果 <code>b1</code> 和 <code>b2</code> 都 taken 了，<code>b3</code> 一定是 not taken</li>
<li>只依靠 <code>b3</code> 分支指令的預測，是一定不會發現這樣的規律的</li>
<li>因此需要在預測 b3 時，將前面 b2 和 b1 的執行結果也考慮進去，這就是<strong>基於全局歷史 (Global History) 的分支預測</strong></li>
</ul>
</li>
<li>
<p>基於全局歷史分支預測方法也需要一個暫存器，用來記錄程式中所有分支指令在過去的執行結果，這個暫存器稱為：<code>GHR (Global History Register)</code></p>
</li>
<li>
<p>現實中，GHR 不可能記錄所有分支指令的執行結果，因為 GHR 的寬度是不可能無限大的，因此一般都是使用一個有限 bits 的 GHR，來紀錄最近分支指令的執行結果</p>
<ul>
<li>每當執行一條分支指令時，就將其分支結果 (1: Taken，0: Not taken)，從右 shift 進 GHR</li>
</ul>
</li>
<li>
<p>使用基於全局歷史分支預測時，每當要對一條分支指令進行預測，就會根據當前 GHR 的值來預測，此時仍需要借助 PHT (i.e. <code>2-bit saturating counter</code>)，e.g.</p>
<ul>
<li>當 <code>b3</code> 分支要執行時，如果 GHR 最低 2 bits 的值是 <code>11</code> (i.e. <code>b1</code> 和 <code>b2</code> 都是 taken, <code>b3</code> 一定會是 not taken)，其對應的 PHT 在經過一段訓練的時間後就會達到飽和狀態 (e.g. Strongly not take)，之後就可以準確的預測 <code>b3</code> 的分支結果為 not taken 了</li>
<li>但當 <code>b3</code> 分支要執行時，如果 GHR 最低 2 bits 的值不是 <code>11</code>，那麼 <code>b3</code> 有可能會跳轉 (e.g. <code>aa = 2</code> → <code>aa = 0</code>, <code>bb = 3</code>)，也可能不會跳轉 (e.g. <code>aa = 2</code> → <code>aa = 0</code>, <code>bb = 0</code>)，這種並沒有規律的情況，分支預測的準確度就沒辦法保證了</li>
</ul>
</li>
<li>
<p>基於全局歷史分支預測最理想的方法，就是所有的分支指令都對應一個 PHT，並根據 GHR 的值來索引 PHT entry：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2017.png"></p>
<ul>
<li>由於每個分支指令都有自己的 PHT，因此即使 GHR 的值相同，不同條的分支指令也不會索引到同一個 PHT</li>
<li>但很顯然的，這種設計方法太佔硬體空間，在現實中是不可能使用的</li>
</ul>
</li>
<li>
<p>一般都會使用 hash 將 PC 值進行處理後，得到較小 bits 數的 hash 值後，再用來索引 PHT：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2018.png"></p>
<ul>
<li>P.S. 當 BHT 中的 BHR 個數減少到只有一個 BHR 時，基於局部歷史 (Local History) 的分支預測法就跟基於全部歷史 (Global History) 的分支預測法一樣了
<ul>
<li>i.e. 此時 BHR 跟功能跟 GHR 一樣</li>
</ul>
</li>
</ul>
</li>
<li>
<p>上述方法中，其實有很多的 PHT entry 都沒有被使用到，因此可以採用一種更為簡單的設計方式：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2019.png"></p>
<ul>
<li>缺點：
<ul>
<li>如果兩條分支指令在執行時的 GHR 值相同，那麼就會對應到同一個 PHT entry，造成衝突
<ul>
<li>如果兩條分支指令的跳轉結果是不相同的，那就會對分支預測造成負面影響</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>為了解決這個問題，可以同樣將分支指令部份的 PC 值和 GHR 值做拼接或 XOR，得到的值再拿來索引 PHT entry，就不會對應到同一個 PHT entry 了：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2020.png"></p>
<ul>
<li>同樣的，現實處理器會使用更複雜的演算法，盡量避免 PHT aliasing 的情況發生</li>
</ul>
</li>
<li>
<p>使用基於全局歷史的分支預測法，並無法對分支指令：TNTNTN… 進行完美的預測</p>
<ul>
<li>因為此時分支指令反而會受到其他分支指令的影響</li>
<li>且每次遇到這條分支指令時，都需要重新對 PHT 進行訓練，進而降低了分支預測的準確度</li>
</ul>
</li>
<li>
<p>造成這樣的現象，就是因為有些分支指令適合使用基於局部歷史的分支預測法，但另外一些分支指令適合使用基於全局歷史的分支預測法，如果能根據實際狀況，對不同的分支指令使用不同的分支預測法，那就可以得到更高的預測準確度</p>
</li>
</ul>
<h2 id="424---競爭的分支預測">4.2.4 - 競爭的分支預測</h2>
<ul>
<li>
<p>由於有些分支指令適合使用基於局部歷史的分支預測法，但另外一些分支指令適合使用基於全局歷史的分支預測法，因此可以設定一種自我調整 (self-adapting) 的分支預測方法，根據不同的分支指令執行情況自動選擇是要基於局部歷史或是全局歷史的分支預測法，稱為<strong>競爭的分支預測</strong> (<code>Tournament Predictor</code>)，就像兩種分支預測方法在進行競爭一樣：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2021.png"></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2022.png"></p>
</li>
<li>
<p><code>CPHT (Choice PHT)</code> 是由分支指令 PC 值來索引的一個表格，類似於 PHT，也是由多個 <code>2-bit saturating counter</code> 所組成</p>
<ul>
<li>將分支指令部份的 PC 值，和 GHR 的值，做一定的處理 (e.g. XOR)，用來索引 CPHT</li>
</ul>
</li>
<li>
<p>CPHT 中每個 <code>2-bit saturating counter</code> 的 FSM 如下：</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2023.png"></p>
<ul>
<li>P1 (Global) 預測正確，且 P2 (Local) 預測錯誤時，i.e. <code>1/0</code>，counter 值 <strong>- 1</strong></li>
<li>P1 (Global) 預測錯誤，且 P2 (Local) 預測正確時，i.e. <code>0/1</code>，counter 值 <strong>+ 1</strong></li>
<li>當 P1 (Global) 和 P2 (Local) 預測的結果一樣時，i.e. <code>0/0</code> 或 <code>1/1</code>，不管預測正確與否，counter 值都<strong>保持不變</strong></li>
<li><code>00</code> / <code>01</code>：使用 P1 (Global) 的預測結果</li>
<li><code>11</code> / <code>10</code>：使用 P2 (Local) 的預測結果</li>
<li>同一條分支指令，根據 CPHT 的值，有可能有時候使用基於局部歷史的分支預測法，有時候使用基於全局歷史的分支預測法</li>
</ul>
</li>
<li>
<p>考慮以下的程式：</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-2-1"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-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-2-2"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-2">2</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-2-3"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-3">3</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-2-4"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-4">4</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-2-5"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-5">5</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-2-6"><a style="outline:none;text-decoration:none;color:inherit" href="#hl-2-6">6</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-c" data-lang="c"><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (aa <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)  <span style="color:#75715e">// b1
</span></span></span><span style="display:flex;"><span>    a <span style="color:#f92672">=</span> <span style="color:#ae81ff">0</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (bb <span style="color:#f92672">==</span> <span style="color:#ae81ff">0</span>)  <span style="color:#75715e">// b2
</span></span></span><span style="display:flex;"><span>    b <span style="color:#f92672">=</span> <span style="color:#ae81ff">1</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (aa <span style="color:#f92672">==</span> bb) <span style="color:#75715e">// b3
</span></span></span><span style="display:flex;"><span>    c <span style="color:#f92672">=</span> <span style="color:#ae81ff">3</span>;
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li>如果 <code>b1</code> 和 <code>b2</code> 都 taken 了，<code>b3</code> 一定是 taken
<ul>
<li>此時 <code>b3</code> 使用基於全局歷史的分支預測法較為合適</li>
</ul>
</li>
<li>如果 <code>b1</code> 和 <code>b2</code> 中只有一個 taken，<code>b3</code> 一定是 not taken
<ul>
<li>此時 <code>b3</code> 使用基於全局歷史的分支預測法較為合適</li>
</ul>
</li>
<li>如果 <code>b1</code> 和 <code>b2</code> 都 not taken 了，此時 <code>b3</code> 是否 taken 很難預測 (i.e. <code>aa == bb</code> 是否成立無法預測)
<ul>
<li>此時 <code>b3</code> 使用基於局部歷史的分支預測法較為合適</li>
</ul>
</li>
</ul>
</li>
</ul>
<h2 id="425---分支預測的更新">4.2.5 - 分支預測的更新</h2>
<ul>
<li>在實際的 superscalar CPU 中，對於 branch predictor 的更新需要考量到對 pipeline 的影響
<ul>
<li>對於基於局部歷史和基於全局歷史兩種分支分支預測法，什麼時候更新 branch predictor 對分支預測的準確度是會有所影響的</li>
</ul>
</li>
<li>對分支指令的方向預測，需要更新的內容包含兩個方面：
<ol>
<li>歷史暫存器：
<ul>
<li>基於局部歷史的分支預測法：BHR</li>
<li>基於全局歷史的分支預測法：GHR</li>
</ul>
</li>
<li><code>2-bit saturating counter</code>：
<ul>
<li>不論是哪種分支預測法，都需要更新 PHT 中的 <code>2-bit saturating counter</code> (i.e. PHT entry)</li>
</ul>
</li>
</ol>
</li>
<li>更新歷史暫存器：
<ul>
<li>GHR：
<ul>
<li>有三個時間點可以更新 GHR：
<ol>
<li>
<p>Commit stage：當分支指令 commit，要準備 retire 時，根據分支結果更新 GHR</p>
<ul>
<li>最保守，但也是最正確的，此時更新 GHR 一定沒有問題</li>
<li>但 superscaler CPU 的 pipeline 都很深，等分支指令 commit 並準備 retire 時，該分支指令後面已經有很多條指令已經進入 pipeline 了，這些指令都沒辦法享受到正確的分支預測結果</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2024.png"></p>
<ul>
<li>分支指令 Br2 ~ Br5 都使用同一個 GHR 值，沒辦法享受到 Br1 的分支執行結果</li>
</ul>
</li>
<li>
<p>Execution stage：當分支指令方向被計算出來後，根據分支結果更新 GHR</p>
<ul>
<li>對於普通的 CPU 而言，其 pipeline 並不會很深 (e.g. IF, DC, EX, MEM, WB)，因此，在 execution stage 更新 GHR，大部分後續的分支指令都可以享受到此分支指令的執行結果</li>
<li>但對於 superscalar CPU 而言，在 EX 和 IF stages 之間會存在很多的指令</li>
<li>此外：
<ul>
<li>如果是 in-order CPU，如果發生了 exception，在 execution stage 的分支指令是需要被 flushed 的</li>
<li>如果是 out-of-order CPU，在 execution stage 的分支指令有可能是處在 mis-prediction 的錯誤路徑上</li>
</ul>
</li>
<li>因此，分支指令在 execution stage 的更新 GHR 並不一定是正確的，有可能該分支指令實際上根本不會被執行到</li>
</ul>
</li>
<li>
<p>Fetch stage：在 fetch instruction 時，直接進行分支預測並更新 GHR</p>
<ul>
<li><strong>綜合 1. 和 2. 的結果來說，在 fetch stage，直接進行分支預測並更新 GHR，是一個不錯的方法</strong>
<ul>
<li>可以使後續的分支指令都用到最新的 GHR 內容</li>
<li>當一條分支指令預測錯誤時，即使後續的分支指令都使用到了錯誤的 GHR，其實也是沒關係的，因為後續分支指令也會在 mis-prediction 的路徑上，都應該從 pipeline 中被 flushed 掉</li>
<li>在 fetch stage 進行分支預測是 <strong>speculative</strong> 的，當分支預測失敗時，需要有一種機制可以將 GHR 進行修復，使 GHR 可以恢復到正確的值</li>
</ul>
</li>
</ul>
</li>
</ol>
</li>
<li>GHR 的修復，有兩種方法：
<ol>
<li>
<p>Commit stage 修復法：</p>
<ul>
<li>在 pipeline 中的 comit stage 也放一個 GHR，每當一條分支指令 commit / retire 時，就將正確的分支結果，更新到這個 GHR 中，這個 GHR 稱為：<code>Retired GHR</code></li>
<li>因此，在 pipeline 中就有兩個 GHRs：
<ul>
<li>Fetch stage 的 <code>Speculative GHR</code>，採用 speculative 分支預測並更新 GHR</li>
<li>Commit stage 的 <code>Retired GHR</code>，每當一條分支指令 commit / retire 時，就將正確的分支結果，更新到這個 GHR 中</li>
</ul>
</li>
<li>當一條分支指令發生 mis-prediction 時，Speculative GHR 的內容一定是錯誤的，因此需要等到該分支指令進到 commit stage，並更新 Retired GHR 後，將 Retired GHR 的內容，覆蓋掉 Speculative GHR 的內容，就完成了 Speculative GHR 的修復；然後再根據這條分支指令所跳轉的位址，重新 fetch instruction 即可</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2025.png"></p>
<ul>
<li>缺點：
<ul>
<li>會增大 mis-prediction 的 penalty，因為必須等到該分支指令 commit / retire 時，才能重新 fetch instruction
<ul>
<li>尤其是當該分支指令前有 D-cache miss 的 load 指令時，penalty 會更為嚴重，並降低處理器的性能</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>Checkpoint 修復法：</p>
<ul>
<li>在 fetch stage 進行 GHR 更新前，其實就可以先將”舊的” GHR 值給保存起來，這個保存內容就稱為：<code>Checkpoint GHR</code></li>
<li>一旦該分支指令的結果被計算出來後 (e.g. 在 execution stage)，就可以檢查是否發生 mis-prediction；如果發生 mis-prediction，就可以直接用 Checkpoint GHR 的內容，覆蓋掉 Speculative GHR 的內容，並 flush pipeline；然後再根據這條分支指令所跳轉的位址，重新 fetch instruction 即可</li>
</ul>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2026.png"></p>
<ul>
<li>有一個暫存器專門用來儲存 Checkpoint GHR 的內容</li>
<li>由於新的分支結果是從右邊 shift 進 GHR 的，因此只需將 speculative 的預測結果反轉 (taken → not taken / taken → not taken)，從右邊 shift 進 Checkpoint GHR，即為”舊的” Speculative GHR 的值</li>
<li>當 mis-prediction 發生時，就可以用 Checkpoint GHR 的內容，覆蓋掉 Speculative GHR 的內容，即完成 Speculative GHR 的修復</li>
<li>由於分支預測發生在 fetch stage，此時指令仍是 in-order 的，所以 Checkpoint GHR 的值只要使用 FIFO 的方式，寫進暫存器即可</li>
<li>但如果是 out-of-order CPU，在 execution stage 的時候，指令已經是亂序執行了，因此就無法直接照 FIFO 的順序讀取這個暫存器，增加了此暫存器的設計難度</li>
<li>甚至，對於 out-of-order CPU，分支指令的執行結果也有可能是 mis-predict 的，因為該分支指令有可能是在 mis-prediction 的路徑上 (i.e. 更早之前有條分支指令發生 mis-prediction 了，該分支指令根本不應該被執行)
<ul>
<li>所以依舊需要在 commit stage 時，對分支預測的結果進行檢查</li>
</ul>
</li>
<li>因此，在 commit stage，還是需要使用 <code>Retired GHR</code>，當分支指令 commit / retire 時，如果發現分支預測錯了，那就需要將 Retired GHR 的內容，覆蓋掉 Speculative GHR 的內容，並 flush pipeline；然後再根據這條分支指令所跳轉的位址，重新 fetch instruction</li>
<li>Checkpoint 方法除了在 execution stage 可以修復 Speculative GHR，也可以在 commit stage 修復 Speculative GHR，這樣就可以加快分支指令在發生 mis-prediction 時的修復時間，提高處理器的執行效率</li>
</ul>
</li>
</ol>
</li>
</ul>
</li>
<li>BHR：
<ul>
<li>
<p>BHR 的更新也可以是 speculative 的，也可以是 non-speculative 的</p>
</li>
<li>
<p>不過跟 GHR 不一樣的是，BHR 儲存的是當前分支指令自身在過去的執行情況，通常只有在循環體很短的情況下，才有可能出現一條分支指令在 pipeline 的 commit stage 更新 BHR 時，pipeline 中又已經出現這條分支指令並使用 BHR 進行分支預測</p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2027.png"></p>
<p><img alt="image.png" loading="lazy" src="/posts/superscalar-overview-ch4-part1/image%2028.png"></p>
<ul>
<li>假設 <code>bne</code> 指令對應的 BHR 和 PHT 都已經訓練好，也就是現在是 taken，且分支位址也已經存在 BTB 中</li>
<li>在第一個 <code>bne</code> 指令在 commit (retire) stage 更新 BHR 前，所有後續的 <code>bne</code> 指令都使用不到第一個 <code>bne</code> 指令的分支預測結果
<ul>
<li>但這並不會對性能造成太大的影響，因為經過一段的訓練時間後，branch predictor 對於 <code>bne</code> 這個指令都是預測 taken 的，只有在 for-loop 最後一個迴圈結束時會預測錯誤，其他預測結果都是正確的</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>綜合起來：
<ul>
<li><strong>在 fetch stage 更新 GHR 是最合適的</strong>，GHR 使用了指令間的相關性</li>
<li><strong>在 commit (retire) stage 更新 BHR 是最合適的</strong>，可以簡化設計，又不會對處理器的性能造成太大的影響</li>
</ul>
</li>
</ul>
</li>
<li>更新 PHT 的 saturating counter：
<ul>
<li>當一條分支指令比較有規律時，其對應的 saturating counter 總是處在飽和狀態，因此即使是在該分支指令 commit / retire 時更新 PHT 的 saturating counter，也不會對分支預測的準確度造成太大的影響
<ul>
<li>因此，不論是基於局部歷史的分支預測法，或是基於全局歷史的分支預測法，一般都是<strong>在 commit stage (指令 retire 時) 更新 PHT 的 saturating counter</strong></li>
</ul>
</li>
</ul>
</li>
</ul>
]]></content:encoded>
    </item>
  </channel>
</rss>
