純量程式碼依賴各種指令設定的旗標,來根據特定條件分支。 不過,封裝值代表的不是單一數值,而是平行存在的多個數值。 同一個條件可能在某個通道不成立,卻在另一個通道成立。
這就是為什麼 SIMD 程式碼預設是_無分支_的。
封裝比較不依賴旗標,而是通常會在目的運算元中產生一個遮罩。
對每個通道而言,比較結果為真時會把整個通道填滿 1,為假時則填滿 0。
若讀成有號整數,為真的通道是-1,為假的通道是0。
接著就能把這個遮罩與位元運算組合起來,篩選出特定的通道。
純量的cmp具有通用性,因為它能同時設定多個旗標。
接著另一道指令可以取用這些旗標,決定要分支或進行運算。
不過,封裝比較既檢查條件又計算遮罩,因此不具通用性。 比較本身必須明確指定要測試的條件。
做法有兩種:
eq代表相等,gt代表大於。
其他變體則由其中之一的結果組合而成。除了整數比較要使用特定的條件後綴之外,語法結構跟我們先前看過的一樣:
p + cmp + 條件 + 大小(b、w、d或q)。cmp + p + 大小(s或d)。
條件以立即值的形式,當作額外的運算元傳入。如前所述,整數比較只有相等與大於兩種:
| instruction | description |
|---|---|
pcmpeqb, pcmpeqw, pcmpeqd, pcmpeqq
|
逐通道相等 |
pcmpgtb, pcmpgtw, pcmpgtd, pcmpgtq
|
逐通道有號大於 |
movdqa xmm0, [rel scores]
pcmpgtd xmm0, [rel threshold] ; lane i = 0xFFFFFFFF (-1) if scores[i] > threshold[i], else 0
要產生小於比較,就把運算元對調後使用gt:a < b等同於b > a。
請注意,這種比較是有號的。 要進行無號比較,就把兩個運算元的最高位元翻轉。 做法是與一個只有最高位元設為 1 的遮罩做 XOR。
兩個好用的慣用技巧是:
例如:
pxor xmm4, xmm4 ; xmm4 = all zeros
pcmpeqd xmm7, xmm7 ; xmm7 = all ones
全 0 與全 1 是常見的遮罩,分別用來表示「全為假」與「全為真」。
它們也能用來表示封裝的0或封裝的-1,這些是常見的哨兵值。
例如,標記字串結尾的 NUL 就是一個0。
浮點數的通道採用不同形式:只有一道指令cmpps(64 位元通道則用cmppd),條件以立即值傳入:
movaps xmm0, [rel readings]
cmpps xmm0, [rel limits], 1 ; condition 1 is "less than": lane i = all ones if readings[i] < limits[i]
NASM 也有對應到正確立即值的虛擬指令,比較容易記住。
以下所有指令中,px裡的x可以是s(32 位元浮點數)或d(64 位元浮點數):
| 虛擬指令 | 立即值 | 比較 |
|---|---|---|
cmpeqpx |
0 | a == b |
cmpltpx |
1 | a < b |
cmplepx |
2 | a <= b |
cmpunordpx |
3 | a is NaN or b is NaN |
cmpneqpx |
4 | a != b |
cmpnltpx |
5 | a >= b |
cmpnlepx |
6 | a > b |
cmpordpx |
7 | neither a nor b is NaN |
遮罩會把某個條件的結果編碼起來。 接著就能依這個條件,逐一通道在兩組數值之間做選擇。 遮罩為真時,我們取用其中一組數值的通道;為假時,取用另一組:
; result = (a AND mask) OR (b AND NOT mask)
movdqa xmm2, xmm0 ; xmm0 holds the mask, keep a copy
pand xmm2, xmm3 ; xmm2 = a AND mask: lanes of a where mask is true
pandn xmm0, xmm4 ; xmm0 = NOT mask AND b: lanes of b where mask is false
por xmm2, xmm0 ; combine the two halves
請注意,pandn的不對稱性在這裡正好派上用場:遮罩位於目的運算元,被取反後就在同一道指令中從b挑選。
這個模式就是無分支選擇的封裝形式。
每個通道都會被計算出來,最後只由遮罩決定哪個數值留下,完全不使用jcc。
有些指令會直接執行同樣的選擇,從遮罩暫存器中為每個元素讀取一個位元。 它們叫做混合指令:
| 指令 | 元素 | 遮罩來源 |
|---|---|---|
pblendvb |
位元組 | 隱含的xmm0
|
blendvps |
32 位元通道 | 隱含的xmm0
|
blendvpd |
64 位元通道 | 隱含的xmm0
|
請注意,第一道指令遵循整數語法,另外兩道則遵循浮點數語法。 不過,由於這些指令只是單純挑選原始的位元組,因此任何一道都能同時用於整數與浮點數。
對每個元素而言,當對應遮罩元素的最高位元為 0 時,混合會保留目的運算元;為 1 時則取用來源。
只會參考那個最高位元,而比較產生的遮罩正好符合這點,因為它的通道不是全 1 就是全 0。
遮罩暫存器一律是xmm0,而且是隱含的:
movaps xmm0, [rel mask] ; the selecting mask must be in xmm0
movaps xmm1, [rel b] ; destination: kept where the mask bit is clear
blendvps xmm1, [rel a] ; source: taken where the mask bit is set
也可以用pblendvb依比較遮罩挑選其他任何大小的通道。
由於為真通道中的所有位元組都是全 1,pblendvb會把它們全部選進來。
這些指令都在所執行的運算(blend)之後加上v。
這個v代表_變數_,因為選擇並非靜態不變:它取決於某個暫存器。
也有不帶v的變體,改依立即值來選擇。
它們遵循同樣的模式:若立即值的第i個位元為 1,就選取通道i。
SIMD 程式碼雖然強大,卻少了純量程式碼的許多彈性。 在許多情況下,必須從封裝暫存器回到純量指令的世界。
movmsk系列指令是這兩個世界之間的橋樑。
這些指令會把每個通道的最高位元擷取到一般用途暫存器:
| 指令 | 收集的內容 | 結果寬度 |
|---|---|---|
pmovmskb |
16 個位元組各自的最高位元 | 16 位元 |
movmskps |
4 個 dword 各自的最高位元 | 4 位元 |
movmskpd |
2 個 qword 各自的最高位元 | 2 位元 |
若在比較之後使用,每個設為 1 的位元代表一個「為真」的通道,每個為 0 的位元則代表「為假」的通道。
接著就能照常以純量指令操作這個結果。
例如,popcnt可以數出相符的數量,tzcnt則能找出第一個。
一般用途暫存器可以是 32 位元或 64 位元寬。
純量指令test也有封裝版本:ptest。
它與純量版本相似,會對兩個運算元做 AND 運算,但不修改它們。
與test不同的是,ptest還會執行 ANDN 運算,把第一個運算元取反。
因此,ptest可以想成是pand與pandn的不破壞版本,並依結果設定旗標。
與這兩道指令很類似,ptest會把整個 SIMD 暫存器視為單一通道,因此不接受大小前綴。
若 AND 運算的結果是0,就會設定ZF;若 ANDN 運算的結果是0,設定的則是CF。
這表示ptest可以用來同時檢查全 1 與全 0 的遮罩:
ptest用在暫存器自身時,只有在該暫存器為全 0 才會設定ZF。
這模擬了純量常見的慣用技巧:把test用在暫存器自身來檢查是否為0。ptest用在暫存器與全 1 遮罩上時,只有在該暫存器為全 1 才會設定CF。
此外,只有在暫存器為全 0 時才會設定ZF,因此能一次檢查兩種遮罩。pxor xmm0, xmm0 ; all zeros
pcmpeqb xmm1, xmm1 ; all ones
pcmpeqb xmm2, xmm2
ptest xmm0, xmm0 ; ZF set: a register against itself detects all zeros
ptest xmm0, xmm1 ; ZF set, CF clear: xmm0 is all zeros, not all ones
ptest xmm2, xmm1 ; CF is set only if xmm2 is all ones
一如往常,ptest的結果可以用來分支,或搭配setcc、cmovcc這類無分支指令使用。
你負責學校的評分站,一次為一個區塊的班級成績評分。
每個區塊包含 4 個成績,評分站會對區塊中的每個成績套用相同的運算。 分數是 32 位元浮點數。 有幾個步驟會用到遮罩:一個包含 4 個通道的區塊,每個通道不是全為 1(代表該成績的_是_)就是全為 0(代表_否_)。
你有五個任務。 你透過記憶體位址取得運算元。 有些任務會把答案寫到結果位址,有些則直接回傳。
本練習中所有記憶體位址都是 16 位元組對齊。
本練習的計算應使用 SIMD 指令完成。
第一步會依門檻為每個成績評分。 成績的分數嚴格大於門檻時就算及格。 分數小於或等於門檻的則不及格。
實作flag_above_threshold函式,它會建立一個遮罩,分數高於門檻的通道全為 1,其餘通道則全為 0。
這個函式依序接受以下引數:
result:緩衝區的記憶體位址,4 個遮罩通道會寫入其中。scores:分數的記憶體位址,內含 4 個 32 位元一般浮點數(絕不會是NaN)。thresholds:每個通道門檻值的記憶體位址,內含 4 個 32 位元一般浮點數(絕不會是NaN)。scores = {72.0, 55.0, 90.0, 40.0}
thresholds = {60.0, 60.0, 60.0, 60.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
這個函式沒有回傳值。
另一份報告會標示出滿分的成績,也就是達到最高可能分數的那些。
實作flag_perfect函式,它會建立一個遮罩,分數等於其最高分的通道全為 1,其餘通道則全為 0。
這個函式依序接受以下引數:
result:緩衝區的記憶體位址,4 個遮罩通道會寫入其中。scores:分數的記憶體位址,內含 4 個 32 位元一般浮點數(絕不會是NaN)。maxima:每個通道最高分的記憶體位址,內含 4 個 32 位元一般浮點數(絕不會是NaN)。scores = {100.0, 88.0, 100.0, 73.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {0xFFFFFFFF, 0x00000000, 0xFFFFFFFF, 0x00000000}
這個函式沒有回傳值。
每個分數會得到 1 到 3 的等級:
50.0這個及格門檻。實作assign_ranks函式,它會寫入每個分數的等級。
你應該把及格門檻和等級值定義為記憶體中的封裝常數。 前兩個任務中的函式可以重複使用:分數高於門檻時至少是等級 2,等於最高分時則是等級 3。
這個函式依序接受以下引數:
result:緩衝區的記憶體位址,4 個等級會寫入其中,每個等級都是 32 位元無號整數。scores:分數的記憶體位址,內含 4 個 32 位元一般浮點數(絕不會是NaN)。maxima:每個通道最高分的記憶體位址,內含 4 個 32 位元一般浮點數(絕不會是NaN)。scores = {40.0, 75.0, 100.0, 60.0}
maxima = {100.0, 100.0, 100.0, 100.0}
result = {1, 2, 3, 2}
這個函式沒有回傳值。
經過一整年,每個學生都會累積出一個總等級。 評分站會統計全體學生中共有多少個等級低於及格門檻,以便規劃要加開多少堂額外課程。
實作count_failures函式,它會回傳所有區塊中嚴格低於及格門檻的等級數量。
門檻會以一個包含 4 個相同通道的區塊給出,所以你只要載入一次,就能在每個區塊重複使用。
這個函式依序接受以下引數:
ranks:等級的記憶體位址,由整數個 4 通道區塊組成,每個等級都是 32 位元無號整數。block_count:4 通道區塊的數量,一律大於0。pass_threshold:及格門檻的記憶體位址,內含 4 個相同的 32 位元整數。ranks = {1, 2, 3, 1, 2, 2, 1, 3} // 2 blocks
block_count = 2
pass_threshold = {2, 2, 2, 2}
// => 3
這個函式會以 32 位元有號整數回傳該數量。
在成績紀錄歸檔之前,評分站會檢查全體學生是否全數及格:只要任何區塊裡都沒有任何一筆成績不及格,就代表全數及格。
實作all_passed函式,如果所有學生都及格就回傳1,否則回傳0。
當學生在failing陣列中對應的通道全為 0 時,該學生就算及格。
這個函式依序接受以下引數:
failing:不及格遮罩的記憶體位址,由整數個 4 通道區塊組成,每個通道不是全為 1 就是全為 0。block_count:4 通道區塊的數量,一律大於0。failing = {0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000} // 2 blocks
block_count = 2
// => 1
這個函式會以 32 位元有號整數回傳答案,也就是1或0。