先前的主題介紹了 packed 浮點數值:多個數字存放在同一個 xmm 暫存器中,以 lane 為單位平行運算。
同樣的 xmm 暫存器也可以用來存放 packed 的_整數_。
SIMD 浮點數模型的大部分內容,都可以原封不動地套用到 packed 整數上:
不過,語法上有些微差異:
p,用來表示該指令處理的是 packed 資料。add、mul 等)。packed 整數有四種 lane 大小,每種都有自己的後綴:
| lane 寬度 | 位元組 | 128 位元中的 lane 數 | 後綴 |
|---|---|---|---|
| byte | 1 | 16 | b |
| word | 2 | 8 | w |
| dword | 4 | 4 | d |
| qword | 8 | 2 | q |
例如:
| 指令 | 意義 |
|---|---|
paddb |
packed add,8 位元 lane(16 個 lane) |
paddw |
packed add,16 位元 lane(8 個 lane) |
paddd |
packed add,32 位元 lane(4 個 lane) |
paddq |
packed add,64 位元 lane(2 個 lane) |
有些指令輸入的 lane 是一種大小,輸出的卻是另一種大小。 它們遵循同樣的一般慣例,但帶有_兩個_大小後綴。 第一個表示輸入 lane 的大小,第二個表示輸出 lane 的大小:
| 指令 | 意義 |
|---|---|
pmovsxwd |
packed movsx,從 16 位元 lane 轉為 32 位元 lane |
pmuldq |
packed mul,從 32 位元 lane 轉為 64 位元 lane |
有兩個名稱帶有 integer 的指令會在 xmm 暫存器和記憶體之間複製 128 位元:
| 指令 | 說明 |
|---|---|
movdqa |
將 packed 整數複製到或複製自_已對齊_的位置 |
movdqu |
將 packed 整數複製到或複製自_未對齊_的位置 |
它們的行為和 movaps 與 movups 一樣:movdqa 在未對齊的位址上會觸發 fault,而 movdqu 則接受任何位址。
這四個指令都會複製 128 位元,而不會解讀其內容。
名稱帶有 integer 的那一對,只是慣例上搭配整數資料使用,並非强制要求。
這裡的 dq 代表 double-qword,也就是 128 位元(16 位元組)。
加法和減法遵循命名規則:
paddb xmm0, xmm1 ; 16 lanes: each 8-bit, xmm0 += xmm1
paddw xmm2, xmm3 ; 8 lanes: each 16-bit, xmm2 += xmm3
psubd xmm4, xmm5 ; 4 lanes: each 32-bit, xmm4 -= xmm5
psubq xmm6, xmm7 ; 2 lanes: each 64-bit, xmm6 -= xmm7
沒有另外區分有號和無號的版本。
在二補數中,無論 lane 被解讀為有號還是無號,加法和減法都會產生相同的位元,所以一個指令就能同時服務兩種情況。
要如何解讀由你決定,就和 scalar 的 add 與 sub 一模一樣。
這些指令在溢位時會環繞(wrap around),就和它們的 scalar 對應版本一樣。
一個 8 位元的 lane 存放的是對 256 取模的值,所以 200 + 100 的 paddb 會產生 300 - 256 = 44,並捨棄放不下的位元。
請注意,那些多出來的位元不會進位到下一個 lane。 每個 lane 都是與其他 lane 分開運算的,即使它們共用同一個暫存器也一樣。
整數 SIMD 多了一種浮點數 SIMD 沒有的運算:飽和(saturating)加減法,它會_箝制_(clamp)而非環繞。 結果高於 lane 的範圍時,會變成該 lane 能容納的最大值;結果低於範圍時,則會變成最小值。
飽和版本會在大小後綴前插入 s(有號)或 us(無號):
| 指令 | 意義 |
|---|---|
paddsb |
add,飽和,有號 8 位元 lane |
paddusb |
add,飽和,無號 8 位元 lane |
psubsw |
sub,飽和,有號 16 位元 lane |
psubusw |
sub,飽和,無號 16 位元 lane |
箝制的範圍是對應大小與有號性的整數所能表示的最大範圍。 以 byte 為例:
[0, 255]:200 + 100 的 paddusb 會得到 255,而 5 - 10 的 psubusb 會得到 0。[-128, 127]:100 + 50 的 paddsb 會得到 127。當一個 lane 存放的是有界數量時,飽和就很重要,例如像素的色版或音訊取樣。 環繞會讓過亮的像素變暗,而箝制則會讓它維持在最大亮度,這才是你想要的結果。
飽和加減法只適用於 byte 和 word lane,不適用於 dword 或 qword。
兩個 N 位元的值相乘,可能產生 2N 位元的乘積,但目的地的 lane 只有 N 位元寬。 SIMD 運算的處理方式,是明確指定要保留乘積的哪一半,也就是低的 N 位元或高的 N 位元。
以 16 位元 lane 為例,會用到三個指令:
| 指令 | 意義 |
|---|---|
pmullw |
mul,16 位元 lane,保留每個乘積的低 16 位元 |
pmulhw |
mul,16 位元 lane,保留高 16 位元,運算元為有號 |
pmulhuw |
mul,16 位元 lane,保留高 16 位元,運算元為無號 |
乘積的低 16 位元,無論運算元被解讀為有號還是無號都相同,所以只有一個 pmullw。
然而,高 16 位元會依結果的有號性而有所不同。 這就是為何取高半部的乘法要分成有號和無號兩種形式:
pmulhw,用於有號乘法。pmulhuw,多一個 u,用於無號乘法。注意語法:
p,代表 packed 整數。mul。h,表示選取結果中較高(「high」)的位元。u,表示結果應被解讀為無號(也就是不做符號延伸)。w,表示這是 word(16 位元)運算。word 的 packed 乘法完全遵循上述規則。
dword(32 位元)乘法也遵循這個規則,但只有選取低半部的版本:pmulld。
dword 乘法沒有選取高位元的版本。 不過,有幾個版本會將乘法_加寬_,把_偶數索引_的 lane(也就是位置 0 和 2 的 lane)完整的 64 位元乘積存起來:
| 指令 | 意義 |
|---|---|
pmuludq |
將偶數索引的 32 位元元素以無號方式相乘,得到 2 個完整的 64 位元乘積 |
pmuldq |
將偶數索引的 32 位元元素以有號方式相乘,得到 2 個完整的 64 位元乘積 |
注意語法使用了 dq,無號乘法時前面可能還有一個 u。
這是因為這些指令接受 dword lane,並輸出 qword lane。
沒有 packed 整數除法。 需要它的程式碼會把數值轉成浮點數、相除,再轉換回來。
movsx 和 movzx 都有 packed 的對應版本。
它們遵循我們前面提過、輸入與輸出 lane 大小不同的指令所用的相同語法:
pmovsxwd xmm0, xmm1 ; 4 words -> 4 dwords, sign-extended
pmovzxbw xmm0, xmm1 ; 8 bytes -> 8 words, zero-extended
請注意,lane 的數量是由較大的(輸出)寬度決定的。
指令會從來源的低位部分讀取那麼多個 lane。
這和我們在單精度與雙精度浮點數之間的 packed cvt 指令所見到的語意相同。
有些指令可以在 32 位元有號整數與浮點數 lane 之間轉換。
它們遵循 cvt 與 cvtt 指令的慣用語法,但 packed 整數不是用 si,而是用 dq 來表示:
cvtdq2ps xmm0, xmm1 ; convert 32-bit signed integers in xmm1 to 32-bit floats in xmm0
cvtdq2pd xmm2, xmm3 ; convert 32-bit signed integers in xmm3 to 64-bit floats in xmm2
cvtps2dq xmm4, xmm5 ; convert 32-bit floats in xmm5 to 32-bit signed integers in xmm4
使用 pi 代表 packed 整數的指令會寫入舊式的 mmx 暫存器,在 x86-64 上基本上已被淘汰。
請改用 dq 形式,它使用的是 xmm 暫存器。
前面針對浮點數與整數之間 scalar 轉換所做的說明,在這裡同樣適用。
浮點數會依一個稱為 MXCSR 的特殊暫存器進行捨入,而你在函式進入時無法假設它的模式。
有一個 cvtt 版本(多一個 t),它一律會截斷結果。
也可以先使用 round,讓 packed 浮點數值處於已知的狀態再進行轉換。
捨入控制值和 scalar 的 round 相同,packed 浮點數值的語法也一樣:
roundps xmm0, xmm1, 1 ; xmm0 = floor(xmm1), packed 32-bit floats
roundpd xmm2, xmm3, 2 ; xmm2 = ceil(xmm3), packed 64-bit floats
你正在為錄音室裡一台小型混音控台撰寫數位核心。
聲音會以取樣的形式源源不絕地送進來。 每個取樣都是一個數字,描述喇叭單體在某個瞬間的位置。 混音控台一次會處理一整個取樣區塊,對每個取樣套用相同的運算。
你有 5 個任務,每個任務都對記憶體中的一個取樣區塊進行操作。 你會透過記憶體位址接收運算元,並透過結果位址寫回答案。
這個練習的運算應該使用 SIMD 指令完成,而不是純量運算。
錄音室有時會把不同樂器錄在不同的音軌上。 若要透過同一組喇叭播放,你必須把這 2 條音軌相加成一條。
實作mix_tracks函式,它會逐個取樣把兩條音軌相加。
每條音軌都是一個包含 8 個帶正負號 16 位元取樣的區塊。
相加結果若超出帶正負號 16 位元的範圍,必須進行飽和處理,而不是溢位環繞。
這個函式的引數依序如下:
result:用來寫入 8 個混合後取樣的緩衝區的 16 位元組對齊記憶體位址。track_a:第一條音軌的 16 位元組對齊記憶體位址,內含 8 個帶正負號的 16 位元取樣。track_b:第二條音軌的 16 位元組對齊記憶體位址,內含 8 個帶正負號的 16 位元取樣。track_a = {10000, 30000, 20000, -30000, -20000, -100, 100, 0}
track_b = { 5000, 5000, 20000, -5000, -20000, 100, -100, 0}
result = {15000, 32767, 32767, -32768, -32768, 0, 0, 0}
這個函式沒有回傳值。
當 2 支麥克風同時錄音時,每一支都會隱約收到另一個音源的微弱複本。 這個不想要的複本稱為_漏音_。 一旦量測出這個漏音,就必須把它減掉,才能清理這條音軌。
實作remove_bleed函式,它會逐個取樣把漏音區塊從音軌區塊中減掉。
每個區塊都是 8 個帶正負號的 16 位元取樣,而且相減時必須進行飽和處理。
這個函式的引數依序如下:
result:寫入 8 個清理後取樣的記憶體位址(不保證對齊)。track:錄下來的音軌的 16 位元組對齊記憶體位址,內含 8 個帶正負號的 16 位元取樣。bleed:要移除的漏音的記憶體位址,內含 8 個帶正負號的 16 位元取樣(不保證對齊)。track = {10000, -30000, 5000, 32767, 0, 200, -200, 15000}
bleed = { 2000, 5000, 5000, -5, 0, 300, 300, 5000}
result = { 8000, -32768, 0, 32767, 0, -100, -500, 10000}
這個函式沒有回傳值。
每個聲道上頭都有一個音量表,那是一條小小的光條,亮度顯示該聲道有多大聲。 面板上有一個主音量表,它的亮度應該等於兩個聲道加起來的亮度。 你要把這 2 個音量表區塊相加來驅動它。
注意,亮的音量表會維持在最亮,而不會繞回全暗,所以相加時要進行飽和處理。
實作combine_meters函式,它會逐個取樣把兩個音量表區塊相加。
這個函式的引數依序如下:
result:寫入 16 個合併後數值的 16 位元組對齊記憶體位址。meter_a:第一個聲道音量表區塊的 16 位元組對齊記憶體位址,內含 16 個緊密排列的無正負號 8 位元數值。meter_b:第二個聲道音量表區塊的 16 位元組對齊記憶體位址,內含 16 個緊密排列的無正負號 8 位元數值。每個區塊是 16 個無正負號的 8 位元數值。 相加時要飽和在無正負號的範圍內。
meter_a = {200, 100, 50, 255, 0, 128, 10, 240, 1, 2, 3, 4, 5, 6, 7, 8}
meter_b = {100, 100, 200, 1, 0, 128, 10, 20, 1, 2, 3, 4, 5, 6, 7, 8}
result = {255, 200, 250, 255, 0, 255, 20, 255, 2, 4, 6, 8, 10, 12, 14, 16}
這個函式沒有回傳值。
歌曲不該突然中斷,而是要一路淡出到結束。 你手上有一個_增益係數_區塊,每個取樣對應一個係數,這些係數會一路遞減。
每個係數都是一個以65536為分母的分數。
例如,某個取樣的值是16384,它實際上代表16384 / 65536,也就是1 / 4。
最大的可用數值是32767,只比二分之一小一點。
由於65536是2^16,把取樣乘上它的係數,再保留乘積的_高半部_,就會得到sample * gain / 65536。
實作apply_fade函式,它會依係數縮放每個取樣。
這個函式的引數依序如下:
result:寫入所有縮放後取樣的 16 位元組對齊記憶體位址。track:音軌的 16 位元組對齊記憶體位址,內含n個帶正負號的 16 位元取樣。gains:增益係數的 16 位元組對齊記憶體位址,內含n個帶正負號的 16 位元數值,每個數值的範圍是0到32767。n:取樣的數量,永遠不為零且是8的倍數,型別為 64 位元無正負號整數。注意,track是一個陣列,內含n個帶正負號的 16 位元取樣,其中n永遠不為零,且一定是8的倍數。
對每個取樣來說,縮放後的結果是帶正負號乘積sample * gain的_高_16 位元。
track = {20000, 20000, 20000, 20000, -20000, -20000, -20000, -20000}
gains = {32767, 24576, 16384, 8192, 32767, 24576, 16384, 8192}
result = { 9999, 7500, 5000, 2500, -10000, -7500, -5000, -2500}
保留高半部時,捨入方向是朝負無窮大。
這就是為什麼20000 * 32767縮放後會得到9999,而-20000 * 32767會縮放成-10000,而不是-9999。
這個函式沒有回傳值。
鼓手錄得太大聲了。 為了把整條音軌壓下來,你要把每個取樣除以一個常數。
實作attenuate_track函式。
這個函式的引數依序如下:
result:寫入結果的 16 位元組對齊記憶體位址,結果為 32 位元帶正負號整數。samples:取樣的 16 位元組對齊記憶體位址,內含n個帶正負號的 16 位元數值。divisor:16 位元組對齊的記憶體位址,內含除數的 4 份相同複本,內容為 32 位元整數。n:取樣的數量,永遠不為零且是4的倍數,型別為 64 位元無正負號整數。注意,samples是一個陣列,內含n個帶正負號的 16 位元取樣,其中n永遠不為零,且一定是4的倍數。
除數會以同一個 32 位元整數的4份複本提供,事先複製好,讓它能一次填滿全部 4 個資料通道。
整數 SIMD 沒有緊密排列的除法,所以這項工作會改用緊密排列的浮點數來做,再轉回緊密排列的 32 位元整數,存入result。
轉換回來時要朝零截斷,所以商為2.3時會變成2,商為-2.3時會變成-2。
samples = {300, 150, 90, -120, 100, 50, -50, 7}
divisor = {3, 3, 3, 3}
result = {100, 50, 30, -40, 33, 16, -16, 2}
這個函式沒有回傳值。