
在計算機體系中,數據並行有兩種實現路徑:MIMD(Multiple Instruction Multiple Data,多指令流多數據流)和SIMD(Single Instruction Multiple Data,單指令流多數據流)。其中MIMD的表現形式主要有多發射、多線程、多核心,在當代設計的以處理能力為目標驅動的處理器中,均能看到它們的身影。同時,隨着多媒體、大數據、人工智能等應用的興起,為處理器賦予SIMD處理能力變得愈發重要,因為這些應用存在大量細粒度、同質、獨立的數據操作,而SIMD天生就適合處理這些操作。
SIMD結構有三種變體:向量體系結構、多媒體SIMD指令集擴展和圖形處理單元。本文集中圍繞向量體系結構進行描述。
向量體系結構的基本組成
1.簡介
向量體系結構使用一條向量指令開啟一組數據操作,其中數據的加載、存儲以及數據計算以流水線的形式進行。它最大的特點就是:其僅在一組數據操作的第一個元素存在存儲器延遲和由冒險引起的停頓,后續元素會沿着流水線順暢流動。
我們通過一個以cray-1為基礎的向量處理器來分析。它同時包含標量體系結構和向量體系結構,其標量指令集為MIPS,向量指令集則稱為VMIPS。
2.架構組成
VMIPS指令集體系結構的主要組件如下圖所示:

圖1 向量體系結構VMIPS的基本結構
-
向量寄存器:N個向量寄存器,每個向量寄存器都是一個固定長度的寄存器組,用於保存一個向量。向量長度通常為32、64等,這也是單條向量指令所能執行元素的最大值。
-
標量寄存器:用於給向量功能單元提供標量輸入數據、給載入/存儲單元提供地址,或是作為向量長度寄存器、向量遮罩寄存器等功能。
-
向量功能單元:每個單元都完全實現流水化,可以在每個時鍾周期開啟一個新的操作。每個功能單元可能只有單條流水線(單車道),也可以有多個並行的流水線(多車道)。
-
向量載入/存儲單元:向量載入與存儲操作也是完全流水化的,在初始延遲后,可以以每個時鍾周期一個只的帶寬移動字。該單元通常還會處理標量載入和存儲。
3.向量長度寄存器
向量處理器有一個自然向量長度,由向量寄存器的長度決定。然而在實際程序中,特定向量運算的長度在編譯時通常是未知的。例如以下代碼:
for (i=0; i<n; i++)
{
y[i] = a * x[i] + y[i];
}
向量運算的長度取決於n,而n是個變量,它可能在執行時隨應用情況而變化。
向量長度寄存器(VLR)就是為了解決實際數據向量長度變化這一問題,它控制一條向量指令執行元素的長度,但這一長度不能超過自然向量長度。
4.向量遮罩寄存器
考慮如下循環:
for (i=0; i<64; i++)
{
if (y[i] != 0)
{
y[i] = x[i] - y[i];
}
}
由於循環體需要條件執行,因此不可能對一組數據向量的元素執行相同操作,也就不便於對循環進行向量化。
通過啟用向量遮罩寄存器可以解決這一問題。它可以通過布爾向量來控制一條向量指令中每個元素運算的條件執行,如向量中某元素對應的向量遮罩寄存器中的布爾值為1,則執行指令操作,否則執行空操作。
需要注意的一點是,即使遮罩為0的元素,它仍會占用與遮罩為1元素相同的執行時間。
較標量處理器的優勢
向量體系結構與標量體系結構相比,其優勢有:
-
僅在每個向量載入或存儲操作中付出較長的存儲器延遲時間,而不需要在載入或存儲每個元素時耗費時間。
-
減少了流水線互鎖的頻率。多個指令間可能由於相關性而引起停頓,向量體系結構中每條向量指令僅需一次流水線停頓,而不是每個向量元素操作需要一次。
-
大幅縮減了動態指令帶寬。向量體系結構使原本在標量體系中每個元素操作需要一條指令,變成了每個向量操作僅需要一條向量指令。
向量體系結構 & 軟件流水線
在看向量體系結構的指令處理過程時,很容易將它與軟件流水線的概念混淆在一起。實際上二者確實有很多相似之處,我們可以通過一個例子來分析:
for(i=0 ; i<16; i++)
{
sum += tab[i];
}
如上的一個循環體,實現16長度的數組求和。假設處理器中含有並行的加載/存儲單元(.D)與算術單元(.L),.D單元執行一條指令消耗4個時鍾周期,.L單元執行一條指令消耗1個時鍾周期。
在標量處理器中不進行軟件流水線編排,假設不考慮循環操作指令的開銷,其在不同時刻執行指令的過程大致如下所示:
clock .D .L
1 load1
2
3
4
5 load2 add1
6
7
8
9 load3 add2
……
61 load16 add15
62
63
64
65 add16 (結束)
在標量處理器中進行指令軟件流水線編排后,在不同時刻執行指令的過程大致如下所示:
clock .D .L
1 load1
2 load2
3 load3
4 load4
5 load5 add1
6 load6 add2
7 load7 add3
8 load8 add4
10 load9 add5
11 load10 add6
12 load11 add7
13 load12 add8
14 load13 add9
15 load14 add10
16 load15 add11
17 load16 add12
18 add13
19 add14
20 add15
21 add16 (結束)
在向量處理器中,假設向量長度為16,功能單元僅有單車道,在不同時刻執行指令的過程大致如下所示:
clock .D .L
1 vload
2
3
4
5 vadd
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 (結束)
從以上的例子中可以很清楚地看出:
-
軟件流水線和向量指令具有相同的時鍾消耗,它們都僅在處理第一個元素時才發生延遲停頓。
-
軟件流水線並沒有減少指令的個數,而向量指令處理一組向量操作僅需一條向量指令。
總結起來就是:軟件流水線可以減少流水線停頓,但很難大幅縮減指令帶寬要求;而向量體系結構既能減少流水線停頓,也能大幅縮減指令帶寬要求。
也有現代處理器專門針對軟件流水線而設計硬件和指令支持。例如TI C6000架構中的SLOOP BUFFER單元和軟件流水指令,可以將上述軟件流水線例子中clock5~clock17的指令表示成一條load和add指令,稱為軟件流水核。這樣的處理方式也實現了縮減指令帶寬的目的。
因此,宏觀上可以認為:向量體系結構是通過指令集的設計支持,在硬件上實現可向量化循環的流水化操作;軟件流水線是通過編譯器端的指令編排(或是加上部分硬件設計支持),在軟件上實現可向量化循環的流水化操作。
一些現實問題
1.處理非整數倍自然向量長度的向量
在實際程序中,特定向量運算的長度通常都是未知的,比如以下代碼:
for (i=0; i<n; i++)
{
y[i] = a * x[i] + y[i];
}
假設向量體系結構的自然向量長度MVL=64,當n=64k(1,2……),我們可以很方便的把代碼寫成一個向量指令的k次循環。但更有可能的是,n不為64的整數倍,為了應對各種不同的情況,該怎么組織代碼呢?
一種名為條帶挖掘(strip mining)的技術能解決這一問題。條帶挖掘是指生成一些代碼,使每個向量運算都是針對小於或等於MVL的大小來完成的。事實上任何n都可以差分成MVL的整數倍部分和余數部分,於是我們可以創建兩個循環,分別處理這兩個部分。向量體系結構利用向量長度寄存器,在編譯時可以生成一個條帶挖掘循環。
處理上面例子的條帶挖掘C語言的寫法如下:
low = 0;
VL = (n % MVL)
for(j=0; j<=(n/MVL); j++)
{
for(i=low; i<(low+VL); i++)
{
y[i] = a * x[i] + y[i];
}
low += VL;
VL = MVL;
}
如此便可將整個向量分段,第一段的長度為余數部分(n%MVL),后續段長度為MVL。
2.提供充足的內存帶寬:內存組(memory bank)
以下幾個原因造成向量處理器對存儲帶寬需求很大:
-
許多向量處理器單時鍾周期可以產生多個內存訪問操作
-
可能存在多個向量處理器同時獨立訪問同一存儲器系統
-
存儲器組的周期時間通常比處理器周期時間高幾倍
-
多數向量處理器支持非連續數據字的訪問
對於存儲器組而言,通常同一時刻對同一存儲器組只能有一個訪問存在,上面這些因素使得處理器架構師需要設置大量的獨立存儲器組,以滿足同一時刻對多個存儲器組的訪問。
3.非單位步幅存儲訪問
當要訪問的一組向量數據在內存中的位置並不是連續的,稱這時元素的訪問為非單位步幅。現實中有很多這樣的例子,例如對矩陣的訪問:
for(i=0; i<m ; i++)
{
C[i] = 0;
for(j=0; j<n; j++)
{
C[i] += A[i][j] * B[j][i];
}
}
由於在C語言中行元素是依次排列的,因此對矩陣A的訪問就是單位步幅的,而對矩陣B的訪問是非單位步幅的(設j>1)。
向量處理器的主要優勢之一就是能夠訪問非連續存儲器位置,並對其進行調整,放到一個密集結構中。不過,為支持大於1的步幅,會使存儲器系統變得復雜。引入非單位步幅后,就有可能頻繁訪問同一個組,引起存儲器組沖突,從而使某個訪問停頓。
舉個存儲器組沖突的例子:假定有16個存儲器組,組繁忙時間為6個時鍾周期,每個時鍾周期以步幅deta訪問存儲器的一個字(4字節):
Bank | 1 | 2 | 3 |...
Address | 0 1 2 3 | 4 5 6 7 | 8 9 10 11 |...
Address | 64 65 66 67 | 68 69 70 71 | 72 73 74 75 |...
若deta=1,則永遠不會發生存儲器組沖突;若deta=4,則第一次訪問將與第五次訪問發生沖突……
在《計算機體系結構.量化方法研究》這本書中,作者給出了一個判斷是否會產生存儲器組沖突的公式,可能由於筆誤發生了一點小錯誤,原書表達如下:
若滿足以下條件,則會產生組沖突,從而產生停頓:
(組數/步幅與組數的最小公倍數) < 組繁忙時間
實際上用這個公式去套上面的例子,顯然是講不通的。一開始我以為是翻譯錯誤,然后去看了英文原本,書上是這么寫的:

翻譯並沒有問題。我自己推導后,認為正確的表示應該是這樣的:
若滿足以下條件,則會產生組沖突,從而產生停頓:
(步幅與組數的最小公倍數/步幅) < 組繁忙時間
或者
(組數/步幅與組數的最小公約數) < 組繁忙時間
以下是推導過程,如有疏忽,請指正:

4.集中-分散存儲訪問
稀疏矩陣在應用中是很常見的,在稀疏矩陣中,向量的元素通常以某種緊湊形式存儲,然后對其進行間接訪問。我們可能會看到類似下面的代碼:
for(i=0; i<n i++)
{
A[k[i]] = A[k[i]] + C[m[i]];
}
假設索引數組k和m的內容是不連續的,則對矩陣A和C的加載稱為集中操作;對矩陣A的存儲操作稱為分散操作。對此類操作的支持被稱為集中-分散(gather-scatter)。
幾乎所有向量處理器都具備這一功能,這一技術允許以向量模式運行帶有稀疏矩陣的代碼。由於可能不知道k和m的值是離散的,簡單的向量化編譯器可能無法自動實現以上源碼的向量化,需程序員給編譯器發出提示。
參考資料
【1】John L. Hennessy,David A. Patterson . 計算機體系結構:量化研究方法:第5版[M].北京:人民郵電出版社,2013. (原名《Computer Architecture:A Quantitative Approach》)
·END·
歡迎來我的微信公眾號做客:信號君
專注於信號處理知識、高性能計算、現代處理器&計算機體系
技術成長 | 讀書筆記 | 認知升級

幸會~
![]()
你可能還感興趣:
