到目前為止,每個 SIMD 操作都是 逐 lane 進行的。
結果中 lane i 的值,是根據輸入中 lane i 的值計算出來的。
然而,在許多情況下,會需要在 lane 之間搬移值,或是希望這麼做。 例如,對我們需要進行的計算來說,lane 的順序可能是錯亂的。
有許多 SIMD 指令可以用不同的方式跨 lane 搬移資料。
重排會重新排列暫存器中的位元組或 lane,每個目的 lane 的來源 lane 可以位於任何位置。 它們可以將同一個 lane 選到不同的目的地,因此也能用來廣播值。
重排指令會根據其大小和執行領域而有不同的行為。
指令 pshufd 會將其來源的四個 32 位元 lane 重新排列到目的。
選擇方式是 8 位元的立即值,讀取為四個 2 位元欄位,每個目的 lane 一個欄位。 每個欄位會選擇要將四個來源 lane 中的哪一個複製到該目的 lane:
| 欄位 | lane 索引 |
|---|---|
00 |
0 |
01 |
1 |
10 |
2 |
11 |
3 |
立即值中欄位的位置(由右至左讀取)表示所選的 lane 要插入何處。 同一個來源 lane 可以被選擇多次,這就是將單一 lane 廣播到整個暫存器的方式:
; The bit fields are read right-to-left
pshufd xmm1, xmm0, 0b00_01_10_11 ; reverse: lane 0 takes source 0b11 (3), lane 1 takes source 0b10 (2), and so on
pshufd xmm3, xmm2, 0b00_00_00_00 ; broadcast source lane 0 into all four lanes
有兩個浮點數重排指令,遵循相同的一般語法:shuf + p + 大小後綴(s 或 d)。
它們也使用立即值來選擇 lane。
shufps 使用與先前整數重排相同的 2 位元欄位編碼。
然而,由於 128 位元運算元中只有 2 個 64 位元 lane,shufpd 只使用 1 位元欄位編碼。
這些指令與整數版本的不同之處在於,它們是從 兩個 運算元而非一個運算元中取得 lane:
shufps xmm0, xmm1, 0b11_10_01_00 ; xmm0 = {xmm0[0], xmm0[1], xmm1[2], xmm1[3]}
shufps xmm2, xmm3, 0b00_00_00_00 ; xmm2 = {xmm2[0], xmm2[0], xmm3[0], xmm3[0]}
shufpd xmm4, xmm5, 0b0_1 ; xmm4 = {xmm4[1], xmm5[0]}
由於兩個運算元都會饋入結果,它們可以用來一步交錯兩個向量。 如果來源和目的都是同一個暫存器,則每個 lane 都會從中取得。
pshufb 是最通用的重排指令。
雖然 pshufb 和 pshufd 的名稱非常相似,只有大小後綴不同,但它們執行的操作卻大不相同。
首先,pshufd 使用立即值來選擇 lane 位置,而 pshufb 則在來源運算元中使用控制向量。
這個控制向量是一個 xmm 暫存器或 16 位元組的記憶體運算元。
對於 16 個目的 lane 中的每一個,控制向量的低四位元會給出來源位元組索引,範圍從 0 到 15。
如果控制向量的 lane i 含有來源位元組索引 j,則目的的第 j-th 個 lane 會被搬到第 i 個 lane。
這凸顯了這兩個指令的第二個差異。
pshufd 是從不同的來源運算元取得要重排的 lane,而 pshufb 則是在目的中就地進行重排。
section .rodata
align 16
reverse: db 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0
section .text
fn:
pshufb xmm0, [rel reverse]
; xmm0[0] = xmm0[reverse[0]] (xmm0[15])
; xmm0[1] = xmm0[reverse[1]] (xmm0[14])
; ...
; in the end, the bytes in xmm0 are reversed
pshufb 甚至比這更有彈性:它可以清除任何 lane。
如果控制向量的 lane i 的最高位元被設定,則目的的第 i-th 個 lane 會被清零。
這使得 pshufb 在單一指令中同時具備任意位元組排列和選擇性清除的功能。
由於 pshufb 以位元組為單位運作,它也可以用來重排不同大小的 lane,方法是將相鄰的 lane 分組在一起。
例如,它可以用來重排 dword:
section .rodata
align 16
input: dd 13, 25, 37, 49 ; each number takes 4 bytes
reverse_dwords: db 12, 13, 14, 15, 8, 9, 10, 11, 4, 5, 6, 7, 0, 1, 2, 3 ; aligned too, since input spans 16 bytes
section .text
fn:
movdqa xmm0, [rel input] ; xmm0 = {13, 25, 37, 49}
pshufb xmm0, [rel reverse_dwords]
; xmm0 = {49, 37, 25, 13}
解包指令會從兩個運算元取得 lane,並將它們交織在一起,在兩者之間交替。 有整數和浮點數兩種變體,它們大致遵循相同的一般語法結構,但有兩個差異:
l 或 h 後綴,用來表示它是作用於每個運算元的低半部(l)或高半部(h)。bw 或 qdq(16 位元組的大小後綴是 dq,如 movdqu 所示)。punpcklwd xmm0, xmm1 ; p + unpck + l + wd
; this interleaves the first 4 words of xmm0 and xmm1 into 4 dwords
; for each dword in the result, the first word is taken from xmm0 and the second, from xmm1
; xmm0 = {xmm0[0], xmm1[0], xmm0[1], xmm1[1], xmm0[2], xmm1[2], xmm0[3], xmm1[3]}
unpckhps xmm2, xmm3 ; unpck + h + p + s
; this interleaves the high 32-bit floats of xmm2 and xmm3
; xmm2 = {xmm2[2], xmm3[2], xmm2[3], xmm3[3]}
封裝指令朝相反方向運作,將兩個運算元的 lane 合併成目的中一半寬度的 lane。
這些指令沒有 p 前綴。
除此之外,語法結合了我們已經見過的元素:
pack。s 或 u,分別表示輸出值是 帶號 或 不帶號。s 表示 飽和,如同先前概念中看過的飽和運算。unpck 的行為相反)。由於 封裝 操作是縮窄的,因此不需要 l 或 h 後綴:
packssdw xmm0, xmm1 ; pack + s (for signed) + s (for saturating) + dw (dword to word)
packuswb xmm2, xmm3 ; pack + u (for unsigned) + s (for saturating) + wb (word to byte)
這些指令是飽和的,也就是說,輸出值會被限制在較窄的範圍內。 它們的作用範圍是從 dword 到 word,以及從 word 到 byte。 沒有 qword 到 dword 的變體。
請注意,輸入一律被視為帶號。
型別(帶號或不帶號)是針對 輸出 的。
它表示要限制的範圍。
例如,packsswb 會限制在帶號位元組的範圍內,即 [-128, 127]。
結果的低 lane 來自目的運算元,高 lane 則來自來源運算元:
packusdw xmm0, xmm1 ; 8 words, each clamped to 0..65535
; xmm0 = {xmm0[0], xmm0[1], xmm0[2], xmm0[3], xmm1[0], xmm1[1], xmm1[2], xmm1[3]}
這些指令沒有浮點數的對應版本。
到目前為止,我們一直使用 movq/movd 在 SIMD 暫存器和通用暫存器之間搬移資料。
這些指令只能寫入或讀取 SIMD 暫存器的低 lane,而且在寫入時,會清除所有其他 lane。
有些指令可以對任何 lane 執行相同操作,而不僅限於第一個 lane,並且不會動到其他 lane:
兩者都遵循整數語法:p + insr/extr + 大小後綴(b、w、d 或 q)。
在兩種情況下,通用暫存器通常都是 32 位元寬。
只有 pinsrq 和 pextrq 需要 64 位元運算元。
記憶體運算元的大小一律與操作大小相同:pinsrb/pextrb 為 8 位元,pinsrw/pextrw 為 16 位元,依此類推。
pinsrb xmm0, eax, 5 ; replace byte 5 of xmm0 with the low byte of eax
pextrb byte [rdx], xmm0, 5 ; copy byte 5 of xmm0 into the memory location indicated by rdx
請注意,這些指令和 movd、movq 很像,只是搬移原始位元組。
這表示它們也可以用於儲存在記憶體或通用暫存器中的浮點數值。
你負責撰寫軟體影像管線的內層迴圈。這個階段會在紋理和圖層送到螢幕之前,先準備紋理並合成圖層。 這條管線以區塊為單位處理像素,對整個區塊套用相同的運算。
一個像素由四個 1 位元組的通道組成:紅色、綠色、藍色和 alpha,順序就是這樣(RGBA)。
一個區塊是 4 個像素,所以總共是 16 位元組。
你有五個任務。
你會透過記憶體位址接收運算元,並透過結果位址寫出答案。 這個練習裡的所有記憶體位址都是 16 位元組對齊。
這個練習裡的計算應該使用 SIMD 指令來完成,而不是純量運算。
紋理是以 RGBA 儲存的,但這條管線要繪製的幀緩衝區預期每個像素都是 BGRA 順序:紅色和藍色通道互換,綠色和 alpha 通道維持原位。
一張影像會以一連串區塊的形式送達,而每個區塊都以相同的方式轉換。
實作 to_display_order 函式,它會把整張影像從 RGBA 轉換成 BGRA,一次處理一個區塊。
你應該把重新排列通道的控制遮罩定義成記憶體中打包好的常數,並在每個區塊重複使用。
這個函式接受下列引數,順序如下:
result:緩衝區的記憶體位址,轉換後的區塊會寫入其中,每個區塊 16 位元組。pixels:來源區塊的記憶體位址,每個區塊 4 個像素,每個像素 4 位元組,以 RGBA 順序排列。block_count:區塊的數量,一律大於 0。pixels = {200, 64, 32, 255, 10, 20, 30, 40, 0, 0, 0, 255, 12, 34, 56, 78,
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} // 2 blocks
block_count = 2
result = {32, 64, 200, 255, 30, 20, 10, 40, 0, 0, 0, 255, 56, 34, 12, 78,
3, 2, 1, 4, 7, 6, 5, 8, 11, 10, 9, 12, 15, 14, 13, 16}
這個函式沒有回傳值。
為了清除某個區域,或是塗滿一段平坦的範圍,管線會把單一顏色寫進該區域的每個像素。
實作 fill_region 函式,它會用同一個顏色的複本,填滿由 block_count 個區塊組成的區域。
這個函式接受下列引數,順序如下:
result:緩衝區的記憶體位址,填滿後的區塊會寫入其中,每個區塊 16 位元組。color:一個像素的記憶體位址,4 位元組,以 RGBA 順序排列。block_count:要填滿的區塊數量,一律大於 0。color = {18, 52, 86, 120}
block_count = 2
result = {18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120,
18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120, 18, 52, 86, 120}
這個函式沒有回傳值。
兩個單通道的圖層需要合併到一個緩衝區,並讓它們的取樣值交錯排列。 結果會交替出現第一個圖層的一個取樣值,然後是第二個圖層的一個取樣值。
實作 weave_scanlines 函式,它會把兩列各 16 個取樣值,交織成一列 32 個取樣值。
這個函式接受下列引數,順序如下:
result:緩衝區的記憶體位址,32 個交錯後的取樣值會寫入其中。first:第一列的記憶體位址,16 個取樣值,每個都是 8 位元的值。second:第二列的記憶體位址,16 個取樣值,每個都是 8 位元的值。first = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}
second = {100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115}
result = {0, 100, 1, 101, 2, 102, 3, 103, 4, 104, 5, 105, 6, 106, 7, 107,
8, 108, 9, 109, 10, 110, 11, 111, 12, 112, 13, 113, 14, 114, 15, 115}
這個函式沒有回傳值。
增亮的階段會以 16 位元的工作精度縮放每個取樣值,因此過亮的取樣值可以超過 255,而差值可以掉到 0 以下。
最後的階段會把這些工作值縮窄回 8 位元的取樣值以供顯示:低於 0 的值鉗位到 0,高於 255 的值鉗位到 255。
實作 pack_samples 函式,它會依序把兩組各 8 個工作值,縮窄成一列 16 個取樣值。
這個函式接受下列引數,順序如下:
result:緩衝區的記憶體位址,16 個鉗位後的取樣值會寫入其中,每個都是 8 位元的值。first:前 8 個工作值的記憶體位址,每個都是有號 16 位元整數。second:接下來 8 個工作值的記憶體位址,每個都是有號 16 位元整數。first = {300, -5, 128, 255, 0, 400, 64, 200}
second = {255, 256, -1, 100, 50, 1000, 7, 0}
result = {255, 0, 128, 255, 0, 255, 64, 200, 255, 255, 0, 100, 50, 255, 7, 0}
這個函式沒有回傳值。
紋理和頂點資料經常以交錯的形式送達,每個點的 x 和 y 都打包在一起。
不過,為了提升處理效率,常常需要把它們分開:所有的 x 值放在同一個向量裡,所有的 y 值放在另一個向量裡。
實作 split_coordinates 函式,它會把四個交錯的 (x, y) 點,拆分成一個 x 座標向量和一個 y 座標向量。
這個函式接受下列引數,順序如下:
xs:緩衝區的記憶體位址,4 個 x 座標會寫入其中,共 4 個 32 位元浮點數。ys:緩衝區的記憶體位址,4 個 y 座標會寫入其中,共 4 個 32 位元浮點數。first:前兩個點的記憶體位址,4 個 32 位元浮點數,格式為 {x0, y0, x1, y1}。second:接下來兩個點的記憶體位址,4 個 32 位元浮點數,格式為 {x2, y2, x3, y3}。first = {0.0, 0.5, 1.0, 1.5} // {x0, y0, x1, y1}
second = {2.0, 2.5, 3.0, 3.5} // {x2, y2, x3, y3}
xs = {0.0, 1.0, 2.0, 3.0} // {x0, x1, x2, x3}
ys = {0.5, 1.5, 2.5, 3.5} // {y0, y1, y2, y3}
這個函式沒有回傳值。