1. 程式人生 > >體系結構複習1——指令級並行(迴圈展開和Tomasulo演算法)

體系結構複習1——指令級並行(迴圈展開和Tomasulo演算法)

體系結構複習 CH5 指令級並行

5.1 指令級並行概念

5.1.1 指令級並行

指令級並行(ILP)指通過通過流水線等技術實現多條指令同時並行執行的並行技術

實現ILP主要的方法有:

  • 依靠硬體動態發現和開發並行
  • 依靠軟體在編譯時靜態發現並行

5.1.2 指令間相關性

指令間的相關性限制了指令級的並行度,相關性主要分為(真)資料相關、名稱相關和控制相關

(1)資料相關

指令i位於指令j的前面,下面兩種情況下稱指令j資料相關於指令i:

  • 指令i生成的結果可能會被指令j用到
  • 指令j資料相關於指令k,而指令k資料相關於指令j

資料相關在一定程度上限制了ILP,最常用客服相關性的方法是在不改變相關性的條件下通過指令排程消除資料冒險

(2)名稱相關

當兩條指令使用相同暫存器和儲存器位置(稱為名稱),但與該名稱相關的指令之間並沒有資料流動,此時存在名稱相關

主要分為以下兩種情況(指令i位於指令j的前面):

  • 反相關:指令j對指令i讀取的暫存器和儲存器位置進行寫操作時,發生反相關
  • 輸出相關:指令i和指令j對相同暫存器和儲存器位置進行寫操作時,發生輸出相關

名稱相關並不是真正的資料相關,通過暫存器重新命名技術來消除名稱相關

(3)資料冒險

資料冒險是指指令間存在相關性並且這兩條指令相聚非常接近,足以使執行期間的重疊改變相關運算元的訪問順序,資料冒險分成三類:

  • RAW寫後讀:j在i還沒寫入時就讀取同一位置,會讀取舊值
  • WAW寫後寫:j在i還沒寫入時就寫入同一位置,會被i寫入覆蓋(存在於i指令執行時間較長,如浮點運算或乘法指令)
  • WAR讀後寫:j在i還沒讀入時就寫入同一位置,i錯誤讀取新值(一般先讀後寫的指令集不會出現,可能情況是j提前寫而i在流水線後期才讀的情況)

(4)控制相關

控制相關是指分支指令限定了指令i相對於其的執行順序,和分支條件相關的指令必須先於分支指令執行,受控於分支指令的指令必須在分支指令後執行,不受控於分支指令的指令必須在分支指令前執行

5.2 軟體方法的指令級並行——基本塊內的指令級並行

基本塊是指一段順序執行的程式碼,除了入口處沒有其他轉入分支,除了出口處沒有其他轉出分支

考慮一下C語言程式碼:

for (i = 1; i <= 1000; i++) {
    x[i] = x[i] + s;
}

基本塊對應的彙編程式為:

Loop:   LD      F0,0(R1)
        ADDD    F4,F0,F2
        SD      0(R1),F4
        DADDI   R1,R1,#-8
        BNEZ    R1,Loop

遵循以下指令延遲規定:

這裡寫圖片描述

那麼可以直接分析基本塊彙編程式的指令週期延遲(共9個週期):

1Loop:  LD      F0,0(R1)
2       <stall>
3       ADDD    F4,F0,F2
4       <stall>
5       <stall>
6       SD      0(R1),F4
7       DADDI   R1,R1,#-8
8       <stall>
9       BNEZ    R1,Loop

5.2.1 靜態排程

靜態排程是指通過改變指令順序而不改變指令間資料相關來改善指令延遲,把上述R1的遞減改到前面並利用延遲槽技術(設定延遲槽為1)可以讓上述基本快程式碼壓縮到6個週期完成:

1Loop:  LD      F0,0(R1)
2       DADDI   R1,R1,#-8
3       ADDD    F4,F0,F2
4       <stall>
5       BNEZ    R1,Loop
6       SD      8(R1),F4

說明:

  • DADDI讓R1遞減提前,那麼SD中儲存位置是R1+8而不是R1
  • 延遲槽是無論分支是否成功都要執行的指令

5.2.2 迴圈展開

靜態排程能夠大幅提升基本快執行效率(50%),但是還有一個週期的停頓不能消除,那麼由此引入另一種塊內消除延遲方法——迴圈展開

迴圈展開是將迴圈中多個基本塊展開成一個基本塊從而填充stall間隙的方法

將上段基本塊做4段展開,並做排程:

1Loop:  LD      F0,0(R1)
2       LD      F6,-8(R1)
3       LD      F10,-16(R1)
4       LD      F14,-24(R1)
5       ADDD    F4,F0,F2
6       ADDD    F8,F6,F2
7       ADDD    F12,F10,F2
8       ADDD    F16,F14,F2
9       SD      0(R1),F4
10      SD      -8(R1),F8
11      DADDI   R1,R1,#-32
12      SD      16(R1),F12
13      BNEZ    R1,Loop
14      SD      8(R1),F16

平均每個次迴圈僅需要14/4=3.5個cycle,效能大幅增加!

5.2.3 編譯器角度來看程式碼排程

上述最優迴圈展開程式碼排程是我們手工完成的,能夠完成高效的排程是因為我們知道R1代表列舉變數,並知道R1-32前的-16(R1)和16(R1)指向的同一記憶體區域,但編譯器確定對儲存器引用卻不是那麼容易,如其無法確定:

  • 同一次迴圈中,100(R4)和20(R6)是否相等
  • 不同次迴圈中,100(R4)和100(R4)是否相等

5.2.4 迴圈間相關

上面舉例不同次迴圈間是不相關的,但如果出現下述情況:

for (i = 1; i <= 100; i++) {
    A[i] = A[i] + B[i];         //S1
    B[i+1] = C[i] + D[i];       //S2
}

S1和S2沒有相關,但是S1依賴於前一次迴圈的B[i],這種迴圈常不能直接並行執行,需要修改程式碼:

A[1] = A[1] + B[1]; 
for (i = 1; i <= 99; i++) {
    B[i+1] = C[i] + D[i];           //S2
    A[i+1] = A[i+1] + B[i+1];       //S1
}
B[101] = C[100] + D[100];

5.3 硬體方法的指令級並行

之前所瞭解的靜態排程技術存在困難,並且一旦出現無法消除的資料相關,那麼流水線就會停頓,直到清除相關流水線才會繼續流動

動態排程提出動態發射的思想,若流水線即將發生停頓,硬體會動態選擇後續不會違反相關性的指令發射,這也就是常說的順序發射、亂序執行、亂序完成

動態排程有許多優點:

  • 對一種流水線編譯的程式碼可以在不同流水線上高效執行,而不需要針對不同微體系結構重新編譯
  • 動態排程能克服編譯器靜態排程不能確定的相關性(上述難題)
  • 允許處理器容忍一些不確定/動態的延遲,如快取缺失帶來的延遲,靜態排程是無論如何也不能做到這一點的

5.3.1 記分牌動態排程

記分牌演算法把ID段分成兩個步驟:

  • 發射:譯碼,並檢測結構相關
  • 讀運算元:等到沒有資料相關時讀入運算元

其主要思想為,在沒有結構衝突時儘可能的發射指令,若某條指令停頓則選取後續指令中與流水線正在執行和停頓的指令發射

(1)記分牌四階段

階段 內容
Issue發射 如果當前指令所使用的功能部件空閒,並且沒有其他活動的指令(執行和停頓)使用相同的目的暫存器(避免WAW),記分牌發射該指令到功能部件,並更新記分牌內部資料。如果有結構相關或WAW相關,則該指令的發射暫停,並且也不發射後繼指令,直到相關解除。
Read讀運算元 如果先前已發射的正在執行的指令不對當前指令的源運算元暫存器進行寫操作,或者一個正在工作的功能部件已經完成了對該暫存器的寫操作,則該運算元有效。運算元有效時記分牌控制功能部件讀運算元,準備執行。(避免RAW)
Execute執行 接收到運算元後功能部件開始執行。當計算出結果後,它通知記分牌該條指令的執行結束,記分牌記錄
Write寫結果 一旦記分牌得到功能部件執行完畢的資訊後,記分牌檢測WAR相關。如果沒有WAR相關就寫結果,否則暫停該條指令。

(2)記分牌硬體部件

1.Instruction status記錄正在活動的指令處於四階段中的哪一步

2.Function unit status記錄功能部件(完成運算的單元)的狀態,其中每個FU有9個記錄參量:

  • Busy:該功能部件是否正在使用
  • Op:該功能部件當前完成的操作
  • Fi:目的暫存器編號
  • Fj,Fk:兩個源暫存器編號
  • Qj,Qk:產生源運算元Fj,Fk的功能部件
  • Rj,Rk:標識Fj,Fk是否就緒的標誌位

3.Register result status記錄哪個FU對某個暫存器是否進行寫操作(還未寫入),若沒有該域為空

(3)動態排程演算法

有了上述三個部件,就可以完成四階段中一些檢視操作,如FU是否空閒、運算元是否就緒(能否執行)、是否存在WAW等

之所以該演算法稱為記分牌演算法,是因為這三個部件就像公示的記分牌一樣,流水線中各操作都需要去檢視記分牌的狀態並根據執行情況在記分牌中寫入相應引數資訊

將四階段和記分牌控制用虛擬碼的形式給出,wait util是某指令向下階段流水的必要條件,Book keeping是該流水段執行完畢後需要記錄的資訊:

Status Wait Until Book Keeping
Issue !FU.busy && result[D] == null FU.busy = true; FU.op = op; FU.Fi = ‘D’; FU.Fj = ‘S1’; FU.Fk = ‘S2’; Qj = result[S1]; Qk = result[S2]; Rj = (Qj == null ? true : false); Rk = (Qk == null ? true : false); Result[D] == ‘FU’(若是源運算元是立即數或R整數暫存器,對應Rjk直接是yes)
Read Rj && Rk Rj = true; Rk = true;
Execute FU complete record complete cycle
Write 任意其他FU的Fj和Fk不是FU.Fi(WAR)或者他們已經ready “通知”所有Fj和Fk是FU.Fi的其他FU該運算元ready,修改Rj/k為true; Result[FU.Fi] = null; FU.busy = false;

5.3.2 Tomasulo動態排程

另一種動態排程演算法是Tomasulo動態排程演算法,它和記分牌的區別主要有:

  • 控制和快取在記分牌中是集中的,Tomasulo是分佈在各部件中
  • Tomasulo的FU稱做Reservation Station保留站(RS),RS中表示暫存器要麼是暫存器值,要麼是指向RS或Load Buffer(也可以看做特殊的RS)的指標;RS可以完成暫存器重新命名,避免WAR、WAW,RS可以多於暫存器,實現更多編譯器無法完成的優化
  • Register result status中記錄的結果都是RS的名字,而不是暫存器
  • FU計算完畢後通過Common Data Bus(CDB)廣播給所有其他RS,並修改Register result status中記錄
  • Tomasulo可以跨越分支!不僅僅侷限於基本快內部FP操作的亂序執行

Tomasulo受限於CDB的效能,一般採用高速CDB

(1)暫存器重新命名

為什麼暫存器重新命名能夠避免WAR和WAW?事例如下:

DIVD    F0,F2,F4
ADDD    F6,F0,F8
SD      F6,0(R1)
SUBD    F8,F10,F14
MULD    F6,F10,F8

存在下列三個名稱相關:

  • SUBD的目的運算元是F8,ADDD源運算元是F8,存在WAR冒險
  • MULD的目的運算元是F6,SD源的運算元是F6,存在WAR冒險
  • MULD的目的運算元是F6,ADDD目的運算元是F6,存在WAW冒險

用T、S重新命名暫存器,有:

DIVD    F0,F2,F4
ADDD    S,F0,F8
SD      S,0(R1)
SUBD    T,F10,F14
MULD    F6,F10,T

且後續F8都用T代替,那麼有:

  • SUBD寫入T可以先於ADDD讀F8,WAR冒險消除
  • MULD寫入F6可以在SD讀入S之前,WAR冒險消除
  • MULD寫入F6可以在ADDD寫入S之前,WAW冒險消除

(2)部件結構

1.RS的結構和記分牌演算法的FU相似,因為有了暫存器重新命名,它省去了F和R兩個標誌位:

  • Busy:該RS是否正在使用
  • Op:該RS當前完成的操作
  • A:存放儲存器地址,開始存立即數,計算出有效地址後存有效地址
  • Vj,Vk:源運算元的值
  • Qj,Qk:產生源運算元的RS

2.Register result status中存對某一暫存器寫操作的RS名字

(3)三階段

階段 內容
Issue發射 如果對應RS空閒(無結構相關),則發射指令和運算元(RS重新命名避免WAR和WAW)
Execute執行 兩運算元就緒後RS開始執行,若沒準備好隨時監聽CDB以獲取所需的運算元(避免RAW)
Write寫結果 CDB傳送所有結果,並修改Register result status

(4)Tomasulo流水控制

Tomasulo動態排程演算法的偽程式碼表示如下:

1.發射階段:

// rs、rt為源運算元
// rd為目的運算元

void issue() {
    if op == FP operation {
        wait until: RS[r].busy == false; // r是和FP Operation對應的RS編號

        if RegisterStatus[rs].Qi != null {
            RS[r].Vj = null;
            RS[r].Qj = RegisterStatus[rs].Qi;
        } else {
            RS[r].Vj = Register[rs];
            RS[r].Qj = null;
        }

        if RegisterStatus[rs].Qk != null {
            RS[r].Vk = null;
            RS[r].Qk = RegisterStatus[rs].Qi;
        } else {
            RS[r].Vk = Register[rs];
            RS[r].Qk = null;
        }

        RS[r].busy == true;
        RegisterStatus[rd].Qi = r; 
    }

    if op == Load or Store {
        wait until: RS[r].busy == false; // Load Buffer和RS相同資料結構

        if RegisterStatus[rs].Qi != null {
            RS[r].Vj = null;
            RS[r].Qj = RegisterStatus[rs].Qi;
        } else {
            RS[r].Vj = Register[rs];
            RS[r].Qj = null;
        }

        RS[r].A = imm;
        RS[r].busy = true;

        if op == Load { // Load only
            RegisterStatus[rt].Qi = r;
        } else {        // Store only
            if (RegisterStatus[rd].Qi != null) {// 避免WAW
                RS[r].Vk = null;
                RS[r].Qk = RegisterStatus[rt].Qi;
            } else {
                RS[r].Vk = Register[rt];
                RS[r].Qk = null;
            }
        }
    }
}

2.執行階段:

void execute() {
    if op == FP Operation {
        wait until: RS[r].Qj == null && RS[r].Qk == null

        compute result with Operand in Vj and Vk;
    }

    if op == Load or Store {
        wait until: RS[r].Qj = 0 && r is head of load-store queue(每次處理隊頭元素)

        RS[r].A = RS[r].Vj + RS[r].A;

        if op == Load {
            wait until: RS[r].A寫入完成

            Read from Mem[RS[r].A]
        }
    }
}

3.寫結果階段:

void write() {
    if op == FP Operation {
        wait until: Execution of r is complete & CDB available

        for all x in RegisterStatus_Index_Set par-do { 
            // 硬體並行執行,模擬直接for迴圈序列模擬即可
            if RegisterStatus[x].Qi == r {
                Register[x] = result;
                RegisterStatus[x].Qi = null;
            }
        }

        for all x in RS_Index_Set par-do {
            if RS[x].Qj == r {
                RS[x].Vj = result;
                RS[x].Qj = null;
            }

            if RS[x].Qk == r {
                RS[x].Vk = result;
                RS[x].Qk = null;
            }
        }

        RS[r].busy = false;
    }

    if op == Store {
        wait until: Execution of r is complete & RS[r].Qk == null

        Mem[RS[r].A] = RS[r].Vk;
        RS[r].busy = false;
    }
}

5.3.3 Tomasulo處理迴圈

Tomasulo演算法能夠迴圈覆蓋執行,關鍵點在於:

  • 暫存器重新命名:不同迴圈中處理不同的物理儲存位置,暫存器重新命名將暫存器表示為動態的指標,增加了暫存器的個數
  • 整數部件先行:以便能夠發射多個迴圈中操作