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

Wha*_*haa 7 assembly sse simd intrinsics micro-optimization

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


我会解释为什么我问:

  1. 输入:具有k个字节的内存缓冲区,每个字节为0x0或0xff.
  2. 期望输出:对于输入的每两个连续字节,寄存器分别用0x0和包装两个四字(64位)0xffff ffff ffff ffff.
  3. 最终目标:根据输入缓冲区的条目对k个双精度缓冲区进行求和.

注意:输入缓冲区的值0x0和值0xff可以更改为最有用的值,前提是在总和之前屏蔽效果仍然存在.

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

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

谢谢,阿萨夫

Pet*_*des 3

每个字节都是整个双精度数的掩码,因此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
Run Code Online (Sandbox Code Playgroud)

实际使用时,请使用多个累加器。另请参阅微融合和寻址模式:索引寻址模式。

用内在函数编写这个应该很容易。正如其他人指出的那样,只需使用取消引用的指针作为内在函数的参数即可。


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

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

punpck并且pshufd都在 SnB 上使用 p1/p5, 也是如此pmovsxaddpd只能在p1上运行。 andpd只能在p5上运行。嗯,也许PAND会更好,因为它可以在 p0(和 p1/p5)上运行。否则循环中的任何内容都不会使用执行端口 0。如果将数据从整数域移动到 fp 域会产生延迟损失,那么如果我们使用PMOVSX,这是不可避免的,因为这将在 int 域中获取掩码数据。最好使用更多的累加器来使循环比最长的依赖链更长。但请将其保持在 28uop 左右,以适合循环缓冲区,以确保每个周期可以发出 4uop。

关于优化整个事情的更多信息:实际上并不需要对齐循环,因为在 nehalem 和更高版本上它将适合循环缓冲区。

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

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

因此,每个展开 4 循环将需要 4 个时钟周期,再加上 1 uop 的循环开销。在 Nehalem 上,您有一个很小的循环缓存,但没有 uop 缓存,展开可能意味着您必须开始关心解码器吞吐量。然而,在 Sandybridge 之前,每个时钟一个负载可能会成为瓶颈。

对于解码器吞吐量,您可以使用ANDPS代替ANDPD,这样可以少一个字节进行编码。我不知道这是否有帮助。


将其扩展到 256bymm寄存器将需要 AVX2 来实现最简单的实现(对于VPMOVSXBQ ymm)。VPMOVSXBQ xmm通过执行两项操作并将它们与VINSERTF128其他操作组合起来,您可能会在仅 AVX 上获得加速。