簡體   English   中英

AVX替代AVX2的矢量移位?

[英]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,ymmPSRLD 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用於車道交叉廣播。


uop計數:

使用標量代碼進行編譯時常量移位計數,整個事情可能如下所示:

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版本非常討厭,也不是那么令人討厭。 完全變量的矢量版本仍然存在

  • 4 uops來打開計數
  • 4個vpsrld xmm,xmm insns為8 vpsrld xmm,xmm
  • vpblendwvblendps合並3個vblendps以合並這些結果。
  • 完全可變AVX1的總計= 15個融合域uops

因此,全變量向量版本與完全常量存儲/標量shuffle / reload版本一樣糟糕,並且其中存在轉儲停頓。

請注意,僅計算融合域uops並不總是唯一相關的事情。 延遲可能很重要,未融合域中的執行端口壓力可能很重要。


為了比較:

  • Skylake: vpsrlvd ymm, ymm, ymm是1 vpsrlvd ymm, ymm, ymm ,1c延遲,每0.5c吞吐量一個。
  • Haswell / BDW: vpsrlvd ymm, ymm, ymm是3 vpsrlvd ymm, ymm, ymm ,2c延遲,每2c吞吐量一個。

請記住,這是一個256b的矢量。 我所做的所有計數都是針對128b向量的。

在Haswell(而不是SnB / IvB)上,我的SSE版本可能會成為洗牌端口吞吐量的瓶頸。 延遲也會稍微惡化,因為資源沖突限制了它可以利用的insn級別並行性的數量。


左移使用SSE4.1 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.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM