gpt4 book ai didi

c++ - AVX2的 vector 移位的AVX替代方案?

转载 作者:行者123 更新时间:2023-12-02 01:54:45 26 4
gpt4 key购买 nike

在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 vector 执行此操作,但是最好使用更多128b加载/存储,尤其是。如果您具有可以在具有AVX2支持的CPU上运行的AVX2版本。 (现有的仅限AVX1的CPU都恰好具有128b加载/存储数据路径,因此256b加载/存储几乎没有优势。)
从 vector 到标量的往返非常昂贵(在标量存储后重新加载时存储转发停顿,或者很多movd/pextrd/pinsrd),因此即使有些笨重的东西也可能比整数代码更好,这取决于吞吐量是否或延迟在您使用此代码中更重要。
我最好的主意基本上是 vector regs的标量:4个移位(每个不同的移位计数一个)和3个立即混合来组合结果。
更新:想法2:左移32位乘以2count。请参阅此答案的结尾。
如果移位计数不是编译时常量,则需要解压缩移位计数的 vector ,以便将每个移位计数作为 vector 的64b。 (非可变移位指令可以将其计数存储在寄存器中,但它们会查看整个低64b。并且,它们不会像标量移位那样屏蔽(模字大小),而是饱和。
将xmm寄存器的4个元素中的每个元素都隔离在原本为零的目标中是很棘手的。您不能只将它们字节向下移动到底部,因为那会在第二个元素中留下非零字节。
由于这是针对不带AVX2的AVX,因此我假设您具有用于AVX2 CPU的单独版本。因此,对于Intel,此版本将在SnB/IvB上使用。这意味着您有两个128b随机播放单元,而不是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,所以blendps更好(端口选择与 PAND相同)。对于SnB,也许使用两者的组合来混合移位结果。对于内在函数,在整数数据上使用FP指令需要大量转换,这使源代码很难看又难以读取。除非您打算使用性能计数器和微基准调整它使其最适合周围的代码,否则请对snB/IvB使用 pblendw。否则,只需转换并使用 blendps即可。
如果您有一个 [ 0 -1 0 -1 ]掩码,则可以替代, vector AND可以在更多端口上运行,并缩短 xmm3的依赖链。这不足以证明加载或生成 mask 是合理的,因此更喜欢使用移位/混洗/混合来完成所有操作的先前版本。
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(1 uop)比 PSRLD xmm,xmm,xmm(2 uop)便宜。但是,立即计数 PSRLD仅1 uop。 (来自 Agner Fog's insn tables)。
@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 uops。

内存目标版本是14个融合域uops,因为我算了一个将移位计数作为编译时常数的版本。加载到ecx中的次数或 pextr计数将更多,因为每个可变计数偏移量比立即计数偏移量多2 uop。

因此,即使SSE/AVX版本非常讨厌,也不是那么讨厌。全变量 vector 版本仍然
  • 4个字母以解开计数
  • 四个vpsrld xmm,xmm insns
  • 的8点
  • 3点用于vpblendwvblendps合并这些结果。
  • 总数= 15个完全可变AVX1 的融合域uops。

  • 因此,完全可变的 vector 版本仅与完全恒定的store/scalar shuffle/reload版本一样糟糕,并且其中包含存储转发停顿。
    请注意,仅计算融合域uops并不总是唯一相关的事情。延迟可能很重要,未融合域中的执行端口压力可能很重要。

    为了比较:
  • Skylake:vpsrlvd ymm, ymm, ymm是1 uop,1c延迟,每0.5c吞吐量之一。
  • Haswell/BDW:vpsrlvd ymm, ymm, ymm是3微码,2c延迟,每2c吞吐量中的一个。

  • 请记住,这是针对256b vector 的。我所做的所有计数都是针对128b vector 的。
    在Haswell(而不是SnB/IvB)上,我的SSE版本可能会影响随机端口吞吐量。延迟也会变得更糟,因为资源冲突限制了它可以利用的insn级并行度。

    通过使用SSE4.1 pmulld乘以2的幂进行左移。
    在SnB/IvB上,SSE4.1 pmulld是1 uop,5c延迟,每1c吞吐量之一。
    在Haswell上,这是2微秒,10c延迟,每2c吞吐量之一。 (因为Skylake的uops可以在p1和p0上运行,所以其吞吐量是Skylake的两倍)
    诀窍是将移位计数转换为2c。一种方法是使用可变移位。如果您可以重用2c的指数 vector 来移动其他多个 vector ,那么这很好,否则这就是鸡与蛋的问题。
    如果移位计数的范围很小(即0..7),则可以使用SSSE3 pshufb作为LUT将计数 vector 映射到2 ^ c vector 。每个元素低字节中的 0必须变为 1(20),但其他字节中的 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
    英特尔SnB/IvB:3微秒(不算AVX不需要的movdqa)。从移位计数到结果的延迟:7c。从移位数据到结果的延迟:5c。吞吐量:每1c一个(因为所有三个uops都可以在不同的端口上运行)。
    使用Haswell及更高版本:延迟提高了5c。 Penryn/Nehalem在 pmulld上的花费也要比SnB多,但不像Haswell那样糟糕。

    LUT在高位64b中全为零,但是说服编译器只存储相关部分并用movq加载它并非难事。我在这里不做讨论。
    为了处理更大的移位计数,我们可以在 [ D-8 C-8 B-8 A-8 ]中使用多个LUT进行查找,以获取每个32b元素的第二个字节的值,依此类推。等等。请注意,如果 C-8C<8设置了符号位,并且 BLENDVB根据符号位合并被设置。但是,它很昂贵,因此一系列合并可能并不仅仅比使用早期的shift/blend-inmediate方法更好。

    除了掩盖 pshufb结果之外,您还可以添加一个 set1_epi32(1) vector 。然后,LUT中具有非零字节的索引范围将为1..8,而移位计数 vector 中的填充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 | ... ]用作控制掩码。在LUT的每一半中都有正确的数据,这将产生正确的两个字节。

    关于c++ - AVX2的 vector 移位的AVX替代方案?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36637315/

    26 4 0
    Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
    广告合作:1813099741@qq.com 6ren.com