[英]AVX alternative of AVX2's vector shift?
在AVX2中,我們有_mm256_srlv_epi32(a, b)
和_mm256_sllv_epi32(a, b)
用於將'a'中的一組8個值移動'b'中的8個值。 是否有一個使用AVX的有效替代方案,以便我可以留在AVX而不必吐出標量代碼?
AVX1沒有256b整數運算,只有FP。 所以我假設你真的在尋找__m128i _mm_srlv_epi32()
的替代品。 使用extractf128 / insertf128,您可以輕松地為256b向量執行此操作,但最好只使用更多128b加載/存儲,尤其是。 如果你有一個可以在支持AVX2的CPU上運行的AVX2版本。 (現有的僅AVX1 CPU都具有128b加載/存儲數據路徑,因此256b加載/存儲幾乎沒有優勢。)
從向量到標量的往返是很昂貴的(無論是店面轉發標店后重裝時檔,還是有很多的movd
/ pextrd
/ pinsrd
),所以即使一些非常笨重可能仍然比整數代碼更好,這取決於是否吞吐量或延遲在你使用它的代碼中更重要。
我所擁有的最好的想法基本上是矢量regs中的標量:4個移位(每個不同移位計數一個)和3個立即混合來組合結果。
更新 :想法2:左移32位乘以2 計數 。 看到這個答案的結尾。
如果移位計數不是編譯時常量,則需要解包移位計數向量,以便將每個移位計數作為向量的64b。 (非變量移位指令可以在寄存器中進行計數,但它們會查看整個低64b。而不是像標量移位一樣屏蔽(模數字大小),它們會飽和。
將xmm寄存器的4個元素中的每一個隔離在一個零目的地是很棘手的。 你不能只將它們按字節移位到底部,因為這會從第二個元素中留下非零字節。
由於這是針對沒有AVX2的AVX,我假設您有一個單獨的AVX2 CPU版本。 因此對於Intel,此版本將用於SnB / IvB。 這意味着你有兩個128b shuffle單元,而不是Haswell和后來的一個。
## 4 shift-counts in the elements of xmm0 = [ D C B A ]. element 1 isolated in xmm1, etc.
vpsrlq xmm2, xmm0, 32 ; xmm2 = [ 0 D 0 B ]
vpunpckhqdq xmm4, xmm2, xmm0 ; xmm4 = [ D C 0 D ]
vpshufd xmm3, xmm4, 0b01010110 ; xmm3 = [ 0 0 0 C ]
vblendps xmm1, xmm2, xmm0, 0b0001 ; xmm1 = [ 0 D 0 A ]
; or
vpblendw xmm1, xmm2, xmm0, 0b00000011 ; xmm1 = [ 0 D 0 A ]
vblendps
在SnB / IvB上的p0 / 5上運行。 等效的vpblendw
在SnB / IvB上的p1 / p5上運行。 在Haswell / SKL上它是p015與p5,因此混合比更好(與PAND
相同的端口選擇)。 對於SnB,可以使用兩者的組合來混合移位結果。 對於內在函數,在整數數據上使用FP指令需要大量的轉換,這使得源難看且難以閱讀。 除非你打算通過perf計數器和pblendw
基准測試來調整它以適應周圍的代碼, pblendw
只需使用pblendw
進行SnB / IvB 。 否則只需投射並使用blendps
。
如果您有[ 0 -1 0 -1 ]
掩碼可用,則向量AND可以在更多端口上運行,並縮短xmm3
的依賴關系鏈。 這不足以證明加載或生成掩碼的合理性,因此更喜歡使用shift / shuffles / blend進行所有操作的先前版本。
vpcmpeqw xmm5, xmm5,xmm5 ; all-ones
vpsrlq xmm5, xmm5, 32 ; [ 0 -1 0 -1 ]: generate the mask on the fly if desired
vpand xmm1, xmm5, xmm0 ; [ 0 C 0 A ]
vpsrlq xmm2, xmm0, 32 ; [ 0 D 0 B ]
vpunpckhqdq xmm3, xmm1,xmm1 ; [ 0 C 0 C ] ; saves 1B vs. the equivalent pshufd: no imm8 byte
vpunpckhqdq xmm4, xmm2,xmm2 ; [ 0 D 0 D ]
旁注:奇怪的是,在Skylake上, VPSRLVD ymm,ymm,ymm
比PSRLD xmm,xmm,xmm
(2 VPSRLVD ymm,ymm,ymm
)更便宜(1 PSRLD xmm,xmm,xmm
)。 PSRLD
即時計數PSRLD
僅為1 PSRLD
。 (來自Agner Fog的insn表 )。
@ BeeOnRope的測試證實,Agner的延遲數字是從數據輸入到數據輸出,而移位計數不在關鍵路徑上。 從移位計數輸入到數據輸出的延遲為2c(xmm)或4c(ymm),通常用於1c的車道內廣播,而3c用於車道交叉廣播。
movaps [rsp - 16], xmm0
shr [rsp - 16], 3 ; 3 uops with a memory-destination. 5 uops for variable count with a memory destination
shr [rsp - 12], 1
shr [rsp - 8], 4
shr [rsp - 4], 1
movaps xmm0, [rsp - 16] ; store-forwarding stall here from the 4x 32b stores to the 128b load
或者可能是變量計數:
## data in xmm0, shift counts in xmm1, results in xmm2
vmovd eax, xmm0 ; 1 uop
vmovd ecx, xmm1 ; 1 uop
shr eax, cl ; 3 uops because of CISC stupidity
vmovd xmm2, eax ; 1 uop
vpextrd eax, xmm0, 1 ; 2 uops
vpextrd ecx, xmm1, 1 ; 2 uops
shr eax, cl ; 3 uops because of CISC stupidity
vpinsrd xmm2, eax, 1 ; 2 uops
... repeat twice more, for indices 2 and 3
因此,可變計數移位的全寄存器方式是6uops + 9uops * 3,總共33 uop。
內存目標版本是14個融合域uops,因為我計算了一個具有shift-count作為編譯時常量的版本。 將加載或pextr
計數加入ecx會更多,因為每個變量計數移位比立即計數移位多2個uop。
因此即使SSE / AVX版本非常討厭,也不是那么令人討厭。 完全變量的矢量版本仍然存在
vpsrld xmm,xmm
insns為8 vpsrld xmm,xmm
vpblendw
或vblendps
合並3個vblendps
以合並這些結果。 因此,全變量向量版本與完全常量存儲/標量shuffle / reload版本一樣糟糕,並且其中存在轉儲停頓。
請注意,僅計算融合域uops並不總是唯一相關的事情。 延遲可能很重要,未融合域中的執行端口壓力可能很重要。
為了比較:
vpsrlvd ymm, ymm, ymm
是1 vpsrlvd ymm, ymm, ymm
,1c延遲,每0.5c吞吐量一個。 vpsrlvd ymm, ymm, ymm
是3 vpsrlvd ymm, ymm, ymm
,2c延遲,每2c吞吐量一個。 請記住,這是一個256b的矢量。 我所做的所有計數都是針對128b向量的。
在Haswell(而不是SnB / IvB)上,我的SSE版本可能會成為洗牌端口吞吐量的瓶頸。 延遲也會稍微惡化,因為資源沖突限制了它可以利用的insn級別並行性的數量。
pmulld
乘以2的冪。 在SnB / IvB上,SSE4.1 pmulld
是1 pmulld
,5c延遲,每1c吞吐量一個。
在Haswell上,它是2 uops,10c延遲,每2c吞吐量一個。 (Skylake的吞吐量是其兩倍,因為它的uop可以在p1和p0上運行)
訣竅是將班次計數變為2 c 。 一種方法是使用可變班次。 如果你可以重復使用2 c的指數向量來移動多個其他向量,那么這很好,否則它就是雞與蛋的問題。
如果移位計數范圍很小(即pshufb
),則可以使用SSSE3 pshufb
作為LUT將計數向量映射到2 ^ c的向量。 每個元素的低字節中的0
必須變為1
(2 0 ),但其他字節中的0
必須保持為零。
## 1<<8 or higher is 0, in an 8bit element
## xmm5 = _mm_set_epi8(0, 0, ..., 1<<7, ..., 1<<2, 1<<1, 1<<0);
## xmm4 = _mm_set1_epi32(0x000000ff);
## data in xmm0, shift counts in xmm1
movdqa xmm2, xmm5 ; avoid this with AVX
pshufb xmm2, xmm5 ; 2^count
pand xmm2, xmm4 ; zero all but the low byte in each element
pmulld xmm0, xmm2 ; data * 2^count
Intel SnB / IvB:3 uops(不包括AVX不需要的movdqa)。 從班次計數到結果的延遲:7c。 從班次數據到結果的延遲:5c。 吞吐量:每1c一個(因為所有三個uop都可以在不同的端口上運行)。
Haswell和后來:5c更高的延遲。 Penryn / Nehalem對於pmulld
比SnB更多的uop,但沒有像Haswell那樣糟糕的延遲。
LUT在高64b中全為零,但說服編譯器僅存儲相關部分並使用movq加載它並非易事。 我不會在這里討論。
為了處理更大的移位計數,我們可以使用多個LUT和[ D-8 C-8 B-8 A-8 ]
查找來獲取每個32b元素的第二個字節的值等。注意C-8
有如果C<8
設置符號位,並且BLENDVB
根據設置的符號位進行合並。 但是它很昂貴,所以一系列的合並可能不會比使用早期的shift / blend-immediate方法更好。
除了屏蔽pshufb
結果之外,您還可以添加set1_epi32(1)
的向量。 那么LUT中具有非零字節的索引范圍將是1..8,並且移位計數向量中的填充0字節將查找LUT的低元素(應該是0)。 這樣做可以使動態恆定生成更加可行:
## xmm5 = _mm_set_epi8(0, 0, ..., 1<<7, ..., 1<<2, 1<<1, 1<<0, 0);
## data in xmm0, shift counts in xmm1
pcmpeqw xmm4,xmm4 ; all-ones
psubd xmm1, xmm4 ; shift_counts -= -1
movdqa xmm2, xmm5
pshufb xmm2, xmm1 ; 2^count
pmulld xmm0, xmm2 ; data * 2^count
沒有優勢,除非你真的關心在一個較少的insn中動態生成一個常數。 (使用pcmpeqw / psrld 24生成set1_epi32(0xff)很快,但編譯器通常只能在一個insn中執行時動態生成。)
OP在聊天中澄清說問題實際上要簡單得多:被移位的數據是編譯時常量(特別是0xF)。 此外,只需要結果的低8位。
這使得僅用PSHUFB作為LUT實現它是微不足道的,不需要乘法。 請參閱本答案的上一節,使用pshufb做2<<count
。
如果你想要一個32b的結果,你可能會生成[ 0 0 D+8 D | 0 0 C+8 C | ... ]
[ 0 0 D+8 D | 0 0 C+8 C | ... ]
[ 0 0 D+8 D | 0 0 C+8 C | ... ]
用作控制掩碼。 利用LUT的每一半中的正確數據,將產生正確的兩個字節。
只是在混合中拋出另一個想法,如果移位很小(在這種情況下<= 4)那么比較/掩碼/添加操作的序列不是太可怕的低效率並且僅使用SSE2指令:
__m128i mm_sllv_4_epi32(__m128i v, __m128i vcount)
{
const __m128i vone = _mm_set1_epi32(1);
__m128i vtest, vmask;
vtest = _mm_set1_epi32(0);
vmask = _mm_cmpgt_epi32(vcount, vtest);
v = _mm_add_epi32(v, _mm_and_si128(v, vmask));
vtest = _mm_add_epi32(vtest, vone);
vmask = _mm_cmpgt_epi32(vcount, vtest);
v = _mm_add_epi32(v, _mm_and_si128(v, vmask));
vtest = _mm_add_epi32(vtest, vone);
vmask = _mm_cmpgt_epi32(vcount, vtest);
v = _mm_add_epi32(v, _mm_and_si128(v, vmask));
vtest = _mm_add_epi32(vtest, vone);
vmask = _mm_cmpgt_epi32(vcount, vtest);
v = _mm_add_epi32(v, _mm_and_si128(v, vmask));
return v;
}
顯然你仍然需要將它應用到AVX矢量的每一半。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.