1. 程式人生 > >[轉]深入理解 CPU 的分支預測(Branch Prediction)模型

[轉]深入理解 CPU 的分支預測(Branch Prediction)模型

目錄

背景

問題的提出

分析

優化

結論

補充知識

分支預測器

引出

 我寫了一篇關於static key的文章,static key 主要是優化關於指令預取的效能,本想自己搞一篇什麼是預取指令,但是這篇寫的很好,直接轉了,感謝作者的無私。

背景

先來看段c++程式碼,我們用256的模數隨機填充一個固定大小的大陣列,然後對陣列的一半元素求和:

#include <algorithm>
#include <ctime>
#include <iostream>

int main()
{
    // 隨機產生整數,用分割槽函式填充,以避免出現分桶不均
    const unsigned arraySize = 32768;
    int data[arraySize];

    for (unsigned c = 0; c < arraySize; ++c)
        data[c] = std::rand() % 256;

    // !!! 排序後下面的Loop執行將更快
    std::sort(data, data + arraySize);

    // 測試部分
    clock_t start = clock();
    long long sum = 0;

    for (unsigned i = 0; i < 100000; ++i)
    {
        // 主要計算部分,選一半元素參與計算
        for (unsigned c = 0; c < arraySize; ++c)
        {
            if (data[c] >= 128)
                sum += data[c];
        }
    }

    double elapsedTime = static_cast<double>(clock() - start) / CLOCKS_PER_SEC;

    std::cout << elapsedTime << std::endl;
    std::cout << "sum = " << sum << std::endl;
}

編譯並執行:

g++ branch_prediction.cpp
./a.out

在我的macbook air上執行結果:

# 1. 取消std::sort(data, data + arraySize);的註釋,即先排序後計算
10.218
sum = 312426300000

# 2. 註釋掉std::sort(data, data + arraySize);即不排序,直接計算
29.6809
sum = 312426300000

由此可見,先排序後計算,執行效率有進3倍的提高。

為保證結論的可靠性, 我們再用java來測一遍:

import java.util.Arrays;
import java.util.Random;

public class Main
{
    public static void main(String[] args)
    {
        // Generate data
        int arraySize = 32768;
        int data[] = new int[arraySize];

        Random rnd = new Random(0);
        for (int c = 0; c < arraySize; ++c)
            data[c] = rnd.nextInt() % 256;

        // !!! With this, the next loop runs faster
        Arrays.sort(data);

        // Test
        long start = System.nanoTime();
        long sum = 0;

        for (int i = 0; i < 100000; ++i)
        {
            // Primary loop
            for (int c = 0; c < arraySize; ++c)
            {
                if (data[c] >= 128)
                    sum += data[c];
            }
        }

        System.out.println((System.nanoTime() - start) / 1000000000.0);
        System.out.println("sum = " + sum);
    }
}

在intellij idea中執行結果:

# 1. 先排序後計算
5.549553
sum = 155184200000
# 2. 不排序直接結算
15.527867
sum = 155184200000

也有三倍左右的差距。且java版要比c++版整體快近乎1倍?這應該是編譯時用了預設選項,gcc優化不夠的原因,後續再調查這個問題。

問題的提出

以上程式碼在陣列填充時已經加入了分割槽函式,充分保證填充值的隨機性,計算時也是按一半的元素來求和,所以不存在特例情況。而且,計算也完全不涉及到資料的有序性,即陣列是否有序理論上對計算不會產生任何作用。在這樣的前提下,為什麼排序後的陣列要比未排序陣列執行快3倍以上?

分析

想象一個鐵路分叉道口。

clipboard.png

為了論證此問題,讓我們回到19世紀,那個遠距離無線通訊還未普及的年代。你是鐵路交叉口的扳道工。當聽到火車快來了的時候,你無法猜測它應該朝哪個方向走。於是你叫停了火車,上前去問火車司機該朝哪個方向走,以便你能正確地切換鐵軌。

要知道,火車是非常龐大的,切急速行駛時有巨大的慣性。為了完成上述停車-問詢-切軌的一系列動作,火車需耗費大量時間減速,停車,重新開啟。

既然上述過車非常耗時,那是否有更好的方法?當然有!當火車即將行駛過來前,你可以猜測火車該朝哪個方向走。

  • 如果猜對了,它直接通過,繼續前行。
  • 如果猜錯了,車頭將停止,倒回去,你將鐵軌扳至反方向,火車重新啟動,駛過道口。

如果你不幸每次都猜錯了,那麼火車將耗費大量時間停車-倒回-重啟。
如果你很幸運,每次都猜對了呢?火車將從不停車,持續前行!

上述比喻可應用於處理器級別的分支跳轉指令裡:

原程式:

if (data[c] >= 128)
    sum += data[c];   

彙編碼:

cmp edx, 128
jl SHORT [email protected]
add rbx, rdx
[email protected]:

讓我們回到文章開頭的問題。現在假設你是處理器,當看到上述分支時,當你並不能決定該如何往下走,該如何做?只能暫停執行,等待之前的指令執行結束。然後才能繼續沿著正確地路徑往下走。

要知道,現代編譯器是非常複雜的,執行時有著非常長的pipelines, 減速和熱啟動將耗費巨量的時間。

那麼,有沒有好的辦法可以節省這些狀態切換的時間呢?你可以猜測分支的下一步走向!

  • 如果猜錯了,處理器要flush掉pipelines, 回滾到之前的分支,然後重新熱啟動,選擇另一條路徑。
  • 如果猜對了,處理器不需要暫停,繼續往下執行。

如果每次都猜錯了,處理器將耗費大量時間在停止-回滾-熱啟動這一週期性過程裡。
如果僥倖每次都猜對了,那麼處理器將從不暫停,一直執行至結束。

上述過程就是分支預測(branch prediction)。雖然在現實的道口鐵軌切換中,可以通過一個小旗子作為訊號來判斷火車的走向,但是處理器卻無法像火車那樣去預知分支的走向--除非最後一次指令執行完畢。

那麼處理器該採用怎樣的策略來用最小的次數來儘量猜對指令分支的下一步走向呢?答案就是分析歷史執行記錄: 如果火車過去90%的時間都是走左邊的鐵軌,本次軌道切換,你就可以猜測方向為左,反之,則為右。如果在某個方向上走過了3次,接下來你也可以猜測火車將繼續在這個方向上執行...

換句話說,你試圖通過歷史記錄,識別出一種隱含的模式並嘗試在後續鐵道切換的抉擇中繼續應用它。這和處理器的分支預測原理或多或少有點相似。

大多數應用都具有狀態良好的(well-behaved)分支,所以現代化的分支預測器一般具有超過90%的命中率。但是面對無法預測的分支,且沒有識別出可應用的的模式時,分支預測器就無用武之地了。

文首導致非排序陣列相加耗時顯著增加的罪魁禍首便是if邏輯:

if (data[c] >= 128)
    sum += data[c];

注意到data數組裡的元素是按照0-255的值被均勻儲存的(類似均勻的分桶)。陣列data有序時,前面一半元素的迭代將不會進入if-statement, 超過一半時,元素迭代將全部進入if-statement.

這樣的持續朝同一個方向切換的迭代對分支預測器來說是非常友好的,前半部分元素迭代完之後,後續迭代分支預測器對分支方向的切換預測將全部正確。

簡單地分析一下:
有序陣列的分支預測流程:

T = 分支命中
N = 分支沒有命中

data[] = 0, 1, 2, 3, 4, ... 126, 127, 128, 129, 130, ... 250, 251, 252, ...
branch = N  N  N  N  N  ...   N    N    T    T    T  ...   T    T    T  ...

       = NNNNNNNNNNNN ... NNNNNNNTTTTTTTTT ... TTTTTTTTTT  (非常容易預測)

無序陣列的分支預測流程:

data[] = 226, 185, 125, 158, 198, 144, 217, 79, 202, 118,  14, 150, 177, 182, 133, ...
branch =   T,   T,   N,   T,   T,   T,   T,  N,   T,   N,   N,   T,   T,   T,   N  ...

       = TTNTTTTNTNNTTTN ...   (完全隨機--無法預測)

在本例中,由於data陣列元素填充的特殊性,決定了分支預測器在未排序陣列迭代過程中將有50%的錯誤命中率,因而執行完整個sum操作將會耗時更多。

優化

利用位運算取消分支跳轉。
基本知識:

|x| >> 31 = 0 # 非負數右移31為一定為0
~(|x| >> 31) = -1 # 0取反為-1

-|x| >> 31 = -1 # 負數右移31為一定為0xffff = -1
~(-|x| >> 31) = 0 # -1取反為0

-1 = 0xffff
-1 & x = x # 以-1為mask和任何數求與,值不變

故分支判斷可優化為:

int t = (data[c] - 128) >> 31; # statement 1
sum += ~t & data[c]; # statement 2

分析:

  1. data[c] < 128, 則statement 1值為: 0xffff = -1, statement 2等號右側值為: 0 & data[c] == 0;
  2. data[c] >= 128, 則statement 1值為: 0, statement 2等號右側值為: ~0 & data[c] == -1 & data[c] == 0xffff & data[c] == data[c];

故上述位運算實現的sum邏輯完全等價於if-statement, 更多的位運算hack操作請參見bithacks.

若想避免移位操作,可以使用如下方式:

int t=-((data[c]>=128)); # generate the mask
sum += ~t & data[c]; # bitwise AND

結論

  • 使用分支預測: 是否排序嚴重影響performance
  • 使用bithack: 是否排序對performance無顯著影響

這個例子告訴給我們啟示: 在大規模迴圈邏輯中要儘量避免資料強依賴的分支(data-dependent branching).

補充知識

Pipeline

先簡單說明一下CPU的instruction pipeline(指令流水線),以下簡稱pipeline。 Pipieline假設程式執行時有一連串指令要被執行,將程式執行劃分成幾個階段,按照一定的順序並行處理之,這樣便能夠加速指令的通過速度。

絕大多數pipeline都由時鐘頻率(clock)控制,在數位電路中,clock控制邏輯閘電路(logical cicuit)和觸發器(trigger), 當受到時鐘頻率觸發時,觸發器得到新的數值,並且邏輯閘需要一段時間來解析出新的數值,而當受到下一個時鐘頻率觸發時觸發器又得到新的數值,以此類推。

而藉由邏輯閘分散成很多小區塊,再讓觸發器連結這些小區塊組,使邏輯閘輸出正確數值的時間延遲得以減少,這樣一來就可以減少指令執行所需要的週期。 這對應Pipeline中的各個stages。

一般的pipeline有四個執行階段(execuate stage): 讀取指令(Fetch) -> 指令解碼(Decode) -> 執行指令(Execute) -> 寫回執行結果(Write-back).

分支預測器

分支預測器是一種數位電路,在分支指令執行前,猜測哪一個分支會被執行,能顯著提高pipelines的效能。

條件分支通常有兩路後續執行分支,not token時,跳過接下來的JMP指令,繼續執行, token時,執行JMP指令,跳轉到另一塊程式記憶體去執行。

為了說明這個問題,我們先考慮如下問題。

沒有分支預測器會怎樣?

加入沒有分支預測器,處理器會等待分支指令通過了pipeline的執行階段(execuate stage)才能把下一條指令送入pipeline的fetch stage。

這會造成流水線停頓(stalled)或流水線冒泡(bubbling)或流水線打嗝(hiccup),即在流水線中生成一個沒有實效的氣泡, 如下圖所示:

本圖中一個氣泡在編號為3的始終頻率中產生,指令執行被延遲

圖中一個氣泡在編號為3的始終頻率中產生,指令執行被延遲。

Stream hiccup現象在早期的RISC體系結構處理器中常見。

有分支預測期的pipeline

我們來看分支預測器在條件分支跳轉中的應用。
條件分支通常有兩路後續執行分支,not token時,跳過接下來的JMP指令,繼續執行, token時,執行JMP指令,跳轉到另一塊程式記憶體去執行。

加入分支預測器後,為避免pipeline停頓(stream stalled),其會猜測兩路分支哪一路最有可能執行,然後投機執行,如果猜錯,則流水線中投機執行中間結果全部拋棄,重新獲取正確分支路線上的指令執行。可見,錯誤的預測會導致程式執行的延遲。

由前面可知,Pipeline執行主要涉及Fetch, Decode, Execute, Write-back幾個stages, 分支預測失敗會浪費Write-back之前的流水線級數。現代CPU流水線級數非常長,分支預測失敗可能會損失20個左右的時鐘週期,因此對於複雜的流水線,好的分支預測器非常重要。

常見的分支預測器

  • 靜態分支預測器

靜態分支預測器有兩個解碼週期,分別評價分支,解碼。即在分支指令執行前共經歷三個時鐘週期。
詳情見圖:

靜態分支預測器

  • 雙模態預測器(bimodal predictor)

也叫飽和計數器,是一個四狀態狀態機. 四個狀態對應兩個選擇: token, not token, 每個選擇有兩個狀態區分強弱: strongly,weakly。分別是Strongly not takenWeakly not taken, Weakly taken, Strongly taken

狀態機工作原理圖如下:

飽和計數器

圖左邊兩個狀態為不採納(not token),右邊兩個為採納(token)。由not token到token中間有兩個漸變狀態。由紅色到綠色翻轉需要連續兩次分支選擇。

技術實現上可用兩個二進位制位來表示,00, 01, 10, 11分別對應strongly not token, weakly not token, weakly token, strongly token。 一個判斷兩個分支預測規則是否改變的簡單方法便是判斷這個二級制狀態高位是否跳變。高位從0變為1, 強狀態發生翻轉,則下一個分支指令預測從not token變為token,反之亦然。

據評測,雙模態預測器的正確率可達到93.5%。預測期一般在分支指令解碼前起作用。

其它常見分支預測器如兩級自適應預測器,區域性/全域性分支預測器,融合分支預測器,Agree預測期,神經分支預測器等。

相關推薦

[]深入理解 CPU分支預測(Branch Prediction)模型

目錄 背景 問題的提出 分析 優化 結論 補充知識 分支預測器 引出  我寫了一篇關於static key的文章,static key 主要是優化關於指令預取的效能,本想自己搞一篇什麼是預取指令,但是這篇寫的很好,直接轉了,感謝作者的無私。

理解CPU分支預測,提高程式碼效率

摘要: 技術傳播的價值,不僅僅體現在通過商業化產品和開源專案來縮短我們構建應用的路徑,加速業務的上線速率,也會體現在優秀程式設計師在工作效率提升、產品效能優化和使用者體驗改善等小技巧方面的分享,以提高我們的工作能力。 技術傳播的價值,不僅僅體現在通過商業化產品和開源專案來縮短我們構建應用的路徑,加速業務

理解CPU分支預測,提高代碼效率

href ann roc 討論 erro 用戶體驗 工作效率 現在 tde 摘要: 技術傳播的價值,不僅僅體現在通過商業化產品和開源項目來縮短我們構建應用的路徑,加速業務的上線速率,也會體現在優秀程序員在工作效率提升、產品性能優化和用戶體驗改善等小技巧方面的分享,以提高我們

分支預測(Branch Prediction)

分支預測(Branch Prediction)是現代處理器用來提高CPU執行速度的一種手段, 其對程式的分支流程進行預測, 然後預先讀取其中一個分支的指令並解碼來減少等待譯碼器的時間.維基百科上對此的解釋是"a strategy in computer architectu

[]深入理解ajax系列——頭部消息

ive -type function ike 成功 form type 5.5 ebp   每個HTTP請求和響應都會帶有相應的頭部信息,其中有的對開發人員有用。XHR對象提供了操作頭部信息的方法。本文將詳細介紹HTTP的頭部信息 默認信息   默認情況下,在發送XHR

[]深入理解閉包(三)

copy AI strong 查找 cte 分組操作 spa 方法 詳細介紹 嚴格來講,IIFE並不是閉包,因為它並不滿足函數成為閉包的三個條件。但一般地,人們認為IIFE就是閉包,畢竟閉包有多個定義。本文將詳細介紹IIFE的實現和用途 實現   函數跟隨一對圓括號()

優化技巧:提前if判斷幫助CPU分支預測

數組 cpu 摘要: 在stackoverflow上有一個非常有名的問題:為什麽處理有序數組要比非有序數組快?,可見分支預測對代碼運行效率有非常大的影響。要提高代碼執行效率,一個重要的原則就是盡量避免CPU把流水線清空,那麽提高分支預測的成功率就非常重要。分支預測在stackoverflow上有一個非

[]深入理解信號槽機制

困難 creat nec 保持 ssa 指針傳遞 ech 消費 導致 原文不可考 來源鏈接http://blog.csdn.net/liuuze5/article/details/53523463 深入理解信號槽(一) 這篇文章來自於 A Deeper

()深入理解TAILQ隊列

agg 特點 指針 變量 新元素 per 3.2 每一個 rst 又開始看libevent了,發現自己已經忘了它使用的基本數據結構,當初看的時候沒有做筆記,結果就是現在又要重看一遍。 發現一個不錯的文章。這個實現主要的不同是使用了二級指針。 先給出一個二級指針的例子。下面這

()深入理解TAILQ佇列

又開始看libevent了,發現自己已經忘了它使用的基本資料結構,當初看的時候沒有做筆記,結果就是現在又要重看一遍。 發現一個不錯的文章。這個實現主要的不同是使用了二級指標。 先給出一個二級指標的例子。下面這個是個不好的反例,argc需要傳遞tv和time_event進去,所以需要使用結構體。 我們使用

[]深入理解Batch Normalization批標準化

這幾天面試經常被問到BN層的原理,雖然回答上來了,但還是感覺答得不是很好,今天仔細研究了一下Batch Normalization的原理,以下為參考網上幾篇文章總結得出。   Batch Normalization作為最近一年來DL的重要成果,已經廣泛被證明其有效性和重要性。雖然有些細節處理還解釋

cpu 分支預測對效能的影響

cpu 分支預測對效能的影響 現在的 cpu 一般都支援分支預測功能。維基百科中有以下描述: 在計算機體系結構中,分支預測器(英語:Branch predictor)是一種數位電路,在分支指令執行結束之前猜測哪一路分支將會被執行,以提高處理器的指令流水線的效能。使用分支預

理解CPU steal time

哪裡可以看到CPU Steal Time? 你可以使用Linux 的 TOP 命令來看到實時的一些效能指標。CPU相關的其中一行內容如下: top 兩個你可能較為熟悉的是 %id(空閒 百分比) 和 %wa(I/O 等待 百分比)。 如果 %id 很低, 那麼說明CPU的工

()深入理解 CocoaPods

CocoaPods 是開發 OS X 和 iOS 應用程式的一個第三方庫的依賴管理工具。利用 CocoaPods,可以定義自己的依賴關係 (稱作 pods),並且隨著時間的變化,以及在整個開發環境中對第三方庫的版本管理非常方便。 CocoaPods 背後的理念主要體

如何編寫C++以減少CPU分支預測錯誤?

利用Intel的vtune測試Hardware Issue選項可以看到分支預測的情況。一般來講95%的分支預測成功是正常的90%表示還有提高的空間75%表示非常糟糕如何提高分支預測的效率, 下面列舉了一些特例表示可以考慮的優化方向case 1if(t1==0&&a

[]深入理解javascript中的立即執行函式(function(){…})()

( function(){…} )()和( function (){…} () )是兩種javascript立即執行函式的常見寫法,最初我以為是一個括號包裹匿名函式,再在後面加個括號呼叫函式,最後達到函式定義後立即執行的目的,後來發現加括號的原因並非如此。要理解

深入理解 CPU 和異構計算晶片 GPU/FPGA/ASIC

王玉偉,騰訊TEG架構平臺部平臺開發中心基礎研發組,組長為專家工程師Austingao,專注於為資料中心提供高效的異構加速雲解決方案。目前,FPGA已在騰訊海量圖片處理以及檢測領域已規模上線。 隨著網際網路使用者的快速增長,資料體量的急劇膨脹,資料中心對計算的需求也

[]深入理解Java的介面和抽象類

深入理解Java的介面和抽象類   對於面向物件程式設計來說,抽象是它的一大特徵之一。在Java中,可以通過兩種形式來體現OOP的抽象:介面和抽象類。這兩者有太多相似的地方,又有太多不同的地方。很多人在初學的時候會以為它們可以隨意互換使用,但是實際則不然。

效能測試必備知識(5)- 深入理解CPU 上下文切換”

做效能測試的必備知識系列,可以看下面連結的文章哦 https://www.cnblogs.com/poloyy/category/1806772.html   前言 上一篇文章中,舉例了大量程序等待 CPU 排程的場景   靈魂拷問 既然程序是在等待,並沒有執行,為什麼系統的平均負載還是會

效能測試必備知識(7)- 深入理解CPU 使用率”

做效能測試的必備知識系列,可以看下面連結的文章哦 https://www.cnblogs.com/poloyy/category/1806772.html   回顧 CPU 使用率是單位時間內 CPU 使用情況的統計,以百分比的方式展示   靈魂拷問 最常用什麼指標來描述系統的 CPU 效