7

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


我将解释我为什么问:

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

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

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

  1. 将输入掩码缓冲区从 8 位扩展到 64 位。
  2. 使用扩展掩码屏蔽双打缓冲区。
  3. 对蒙面双打求和。

谢谢,阿萨夫

4

3 回答 3

3

而是与问题本身相切,更多地在评论中填写一些信息,因为评论部分本身太小而无法容纳这个(原文如此!):

至少 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);
}

它将其转换为以下程序集:

.text.startup 部分的反汇编:

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

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

于 2012-04-04T23:31:25.067 回答
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

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

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


要回答您问题的另一部分,关于如何移动数据以使其对齐PMOVSX

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

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

还有更多关于优化整个事情:对齐循环并不是真正需要的,因为在 nehalem 和以后它会适合循环缓冲区。

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

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

因此,每个展开的 4 循环将花费 4 个时钟周期,加上 1 uop 用于循环开销。在 Nehalem 上,您有一个很小的循环缓存但没有 uop 缓存,展开可能意味着您必须开始关心解码器的吞吐量。但是,在 pre-sandybridge 上,每个时钟一个负载可能无论如何都会成为瓶颈。

对于解码器吞吐量,您可能可以使用ANDPS而不是ANDPD,它需要少一个字节来编码。IDK如果有帮助的话。


将其扩展到 256bymm寄存器将需要 AVX2 才能实现最直接的实现(对于VPMOVSXBQ ymm)。VPMOVSXBQ xmm通过做两个并将它们与VINSERTF128或其他东西结合起来,您可能会加速 AVX-only 。

于 2015-06-04T22:21:30.113 回答
2

你看过_mm_extract_epi16 (PEXTRW)_mm_insert_epi16 (PINSRW)吗?

于 2012-04-01T11:37:51.770 回答