访问打包在128位寄存器中的任意16位元素

时间:2012-04-01 11:18:55

标签: assembly sse simd micro-optimization intrinsics

使用英特尔编译器内在函数,给定一个128位寄存器,打包8个16位元素,如何从寄存器中(廉价地)访问任意元素,以便随后使用_mm_cvtepi8_epi64(符号扩展为2)位于寄存器低16位的位元素到两个64位元素?)


我会解释为什么我要问:

  1. 输入:具有k个字节的内存缓冲区,每个字节为0x0或0xff。
  2. 所需输出:对于输入的每两个连续字节,寄存器分别用0x00xffff ffff ffff ffff打包两个四字(64位)。
  3. 最终目标:根据输入缓冲区的条目对k个双精度缓冲区进行求和。
  4. 注意:输入缓冲区的值0x00xff可以更改为最有用的值,前提是在总和之前屏蔽效果仍然存在。

    从我的问题可以明显看出,我目前的计划如下,在输入缓冲区中流式传输:

    1. 将输入掩码缓冲区从8位扩展到64位。
    2. 使用扩展掩码屏蔽双打缓冲区。
    3. 总结蒙面的双打。
    4. 谢谢, 阿萨夫

3 个答案:

答案 0 :(得分:3)

而不是问题本身的切线,更多地填写评论中的一些信息,因为评论部分本身太小而无法容纳这个( sic!):

至少gcc可以处理以下代码:

#include <smmintrin.h>

extern int fumble(__m128i x);

int main(int argc, char **argv)
{
    __m128i foo;
    __m128i* bar = (__m128i*)argv;

    foo = _mm_cvtepi8_epi64(*bar);

    return fumble(foo);
}

将其转换为以下程序集:

Disassembly of section .text.startup:

0000000000000000 :
   0:   66 0f 38 22 06          pmovsxbq (%rsi),%xmm0
   5:   e9 XX XX XX XX          jmpq   .....

这意味着内在函数不需要以内存参数形式出现 - 编译器透明地处理取消引用mem参数并尽可能使用相应的mem-operand指令。 ICC也这样做。我没有Windows机器/ Visual C ++来测试MSVC是否也这样做,但我希望它能够实现。

答案 1 :(得分:3)

每个字节都是整个double的掩码,所以PMOVSXBQ完全符合我们的需要:从m16指针加载两个字节,并将它们符号扩展为xmm寄存器的两个64位(qword)半部分。

# UNTESTED CODE
# (loop setup stuff)
# RSI: double pointer
# RDI: mask pointer
# RCX: loop conter = mask byte-count
    add   rdi, rcx
    lea   rsi, [rsi + rcx*8]  ; sizeof(double) = 8
    neg   rcx  ; point to the end and count up

    XORPS xmm0, xmm0  ; clear accumulator
      ; for real use: use multiple accumulators
      ; to hide ADDPD latency

ALIGN 16
.loop:
    PMOVSXBQ XMM1, [RDI + RCX]
    ANDPD    XMM1, [RSI + RCX * 8]
    ADDPD    XMM0, XMM1
    add      RCX, 2      ; 2 bytes / doubles per iter
    jl       .loop

    MOVHLPS  XMM1, XMM0    ; combine the two parallel sums
    ADDPD    XMM0, XMM1 
    ret

对于实际使用,请使用多个累加器。另请参阅Micro fusion and addressing modes re:索引寻址模式。

用内在函数写这个应该很容易。正如其他人所指出的那样,只需使用解除引用的指针作为内在函数的args。

要回答问题的其他部分,请关于如何移动数据以排列PMOVSX

在Sandybridge和之后,使用RAM中的PMOVSXBQ可能很好。在每个周期不能处理两个负载的早期CPU上,一次加载16B掩码数据,并一次用PSRLDQ xmm1, 2将其移位2个字节,将2个字节的掩码数据放入低2寄存器的字节。或者也许PUNPCKHQDQPSHUFD通过将高64移动到另​​一个reg的低64来获得两个依赖链。您必须检查哪个端口使用哪个端口(shift与shuffle / extract),并查看与PMOVSXADDPD之间的冲突较少。

punpckpshufd都在SnB上使用p1 / p5,pmovsx也是如此。 addpd只能在p1上运行。 andpd只能在p5上运行。嗯,也许PAND会更好,因为它可以在p0(和p1 / p5)上运行。否则循环中的任何内容都不会使用执行端口0.如果将数据从整数移动到fp域会有延迟惩罚,如果我们使用PMOVSX则不可避免,因为这将获得掩码int域中的数据。最好使用更多的累加器来使循环长于最长的依赖链。但要保持在28uops左右以适应循环缓冲区,以确保每个周期可以发出4个uop。

更多关于优化整个事情: 不需要对齐循环,因为在nehalem以后它将适合循环缓冲区。

您应该将循环展开2或4,因为前Haswell Intel CPU没有足够的执行单元来在一个周期内处理所有4个(融合的)微操作。 (3个向量和一个融合add / jl。两个负载与它们所属的向量uops融合。)Sandybridge以后可以在每个周期执行两个加载,因此每个周期一次迭代是可行的,除了循环开销。

哦,ADDPD的延迟为3个周期。因此,您需要展开并使用多个累加器,以避免循环携带的依赖关系链成为瓶颈。可能会以4为单位展开,然后在最后总结4个累加器。即使使用内在函数,您也必须在源代码中执行此操作,因为这会改变FP数学运算的顺序,因此编译器在展开时可能不愿意这样做。

因此,每个展开的4个循环将花费4个时钟周期,加上1个uop用于循环开销。在Nehalem,你有一个很小的循环缓存但没有uop缓存,展开可能意味着你必须开始关心解码器吞吐量。然而,在预沙桥上,每个时钟一个负载可能会成为瓶颈。

对于解码器吞吐量,您可以使用ANDPS而不是ANDPD,这需要少一个字节进行编码。 IDK,如果这会有所帮助。

将此扩展为256b ymm寄存器需要AVX2才能实现最直接的实现(对于VPMOVSXBQ ymm)。您可以通过执行两个VPMOVSXBQ xmm并将它们与VINSERTF128或其他内容组合来获得AVX的加速。

答案 2 :(得分:2)