成績表

成績表

學習練習

簡介

SIMD:遮罩與條件

純量程式碼依賴各種指令設定的旗標,來根據特定條件分支。 不過,封裝值代表的不是單一數值,而是平行存在的多個數值。 同一個條件可能在某個通道不成立,卻在另一個通道成立。

這就是為什麼 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。

Note

兩個好用的慣用技巧是:

  1. 把暫存器與自己做 XOR,產生全 0。
  2. 把暫存器與自己比較,產生全 1。

例如:

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會把它們全部選進來。

Note

這些指令都在所執行的運算(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 的遮罩:

  1. 把ptest用在暫存器自身時,只有在該暫存器為全 0 才會設定ZF。 這模擬了純量常見的慣用技巧:把test用在暫存器自身來檢查是否為0。
  2. 把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 位元組對齊。

Note

本練習的計算應使用 SIMD 指令完成。

1. 標記高於門檻的分數

第一步會依門檻為每個成績評分。 成績的分數嚴格大於門檻時就算及格。 分數小於或等於門檻的則不及格。

實作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}

這個函式沒有回傳值。

2. 標記滿分

另一份報告會標示出滿分的成績,也就是達到最高可能分數的那些。

實作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}

這個函式沒有回傳值。

3. 指定等級

每個分數會得到 1 到 3 的等級:

  • 等級 1:分數等於或低於50.0這個及格門檻。
  • 等級 2:分數高於該門檻但未達最高分。
  • 等級 3:滿分,也就是等於最高分。

實作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}

這個函式沒有回傳值。

4. 計算不及格的數量

經過一整年,每個學生都會累積出一個總等級。 評分站會統計全體學生中共有多少個等級低於及格門檻,以便規劃要加開多少堂額外課程。

實作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 位元有號整數回傳該數量。

5. 大家都及格了嗎?

在成績紀錄歸檔之前,評分站會檢查全體學生是否全數及格:只要任何區塊裡都沒有任何一筆成績不及格,就代表全數及格。

實作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。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
x86-64 Assembly Exercism

準備好開始 成績表 了嗎?

註冊 Exercism,透過 22 個概念130 個練習 和真人引導來學習並精通 x86-64 Assembly,全部免費。