【问题标题】:Accessing arbitrary 16-bit elements packed in a 128-bit register访问封装在 128 位寄存器中的任意 16 位元素
【发布时间】:2012-04-15 09:02:18
【问题描述】:

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


我会解释我为什么问:

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

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

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

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

谢谢, 阿萨夫

【问题讨论】:

  • pmovsxbq 实际上可以获取内存操作数并直接从内存中加载这两个字节。但当然,MSVC 团队并不关心这一点。
  • @harold 是的,英特尔提供的内在函数实际上缺少地址模式。所以实际上应该归咎于英特尔,而不是 MS(因为我讨厌这样说 ;-))。简单的解决方案是在内联汇编中使用pmovsxbq。否则一次读取 16 个字节和一些 pshufb 将字节放到正确的位置就可以了。
  • @drhirsch 这出乎意料..感谢您告诉我
  • @drhirsch, @harold:请参阅下面的答案 - 只需使用内部传递一个取消引用的指针即可。至少gccicc 想办法做正确的事。

标签: assembly sse simd micro-optimization intrinsics


【解决方案1】:

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

# 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:索引寻址模式。

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


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

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

punpckpshufd 都在 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。


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

【讨论】:

  • 我正在寻找以下转换。它是 2 个 XMM 词到 4 个 XMM 词,X 表示“不关心”。您是否看到了一种有效的方法? [A1 A2 A3 A4][B1 B2 B3 B4] ... => [A1 B1 X X][A2 B2 X X][A3 B3 X X][A4 B4 X X]。我尝试过pinsrdpextrd,但它们的开销超出了我的承受能力。
  • @jww: r0 = punpckldq(v0,v1) / r1 = 使用movhlpspunpckhqdq 提取r0 的高半部分。对 A3/B3 和 A4/B4 输出重复 punpckHdq。如果没有 AVX,您将需要一个 movdqa 来避免破坏第一个输入,以便您可以打开低位和高位。
  • 我在看一些你以前帮助过我的旧笔记。你觉得_mm_shuffle_ps_MM_SHUFFLE 怎么样?缺点是,我必须做四次。
  • @jww:你肯定需要 4 个指令来产生 4 个输出,就像我的 4 指令解决方案一样。 x86 没有任何多输出 SIMD 指令。 _mm_shuffle_ps 与 AVX 很好:所有 4 个都可以从原始源中读取,而不是依赖于以前的源。但是如果没有 VEX 3 操作数编码,它似乎比 _mm_unpacklo/hi_ps_mm_movehi_ps(tmp, vec_with_high_half)(或 unpcklps / movhlps 的任何内在函数)没有任何优势。 Using movhlps with a well-chosen destination variable can save a movaps
  • 我们最终使用了解包。所有路径似乎都需要其中的 4 个。解包获胜并且不需要 *_ps 演员表。另外,我们需要来自 SSSE3 的_mm_shuffle_epi8,因此我们不限于 MMX/SSE/SSE2。另见cham-simd.cpp
【解决方案2】:

与问题本身相切,更多填写有关 cmets 的一些信息,因为评论部分本身太小,无法容纳此(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);
}

它将它变成以下程序集:

.text.startup 部分的反汇编:

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

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

【讨论】:

  • 对此不太确定。汇编形式不需要任何对齐,它需要一个指向单词的指针(movwmov WORD PTR)。即使指针未对齐,编译器也会发出 pmovsxbq 吗?无论如何,这是一个比 Paul R.`s 更好的答案,这对于这种情况是无用的。
  • 好的,我现在看到指针实际上是未对齐的。抱歉打扰了:-)
  • @drhirsch:上面的内容当然是人为的——只是为了说明如果给内在函数一个取消引用的指针作为参数,编译器将发出pmovsxbq (...), %xmm..。我只是选择了一个任意可用的非NULL 指针;-)
【解决方案3】:

【讨论】:

  • 我做到了,我猜这些可能会输出到内存而不是寄存器,从而使一切变慢。我错了吗?编译器 (MSVC) 会解决这个问题吗?
  • 否 - 这些指令直接在 SSE (xmm) 寄存器和普通寄存器之间运行。如果您查看为例如生成的代码_mm_set_epi16 你会看到它只是生成了一个PINSRWs 的字符串。
  • @Anonymous down-voter:您愿意就您认为上述答案不恰当或无用的原因添加评论吗?
  • 答案没有帮助,因为您没有在OP给出的场景中解释如何使用pextrwpinsrw。唯一的方法是看到将循环展开 8 次(因为pextrw 只需要立即数),将 16 位值移动到 gpr,返回到 xmm 寄存器,然后用 pmovsxbq 扩展以在双打。如果这两条指令的简单用法我看不到,请解释一下。
  • @drhirsch:抱歉 - 我认为这很明显 - 我不确定它是否值得投反对票 - OP 似乎发现它很有帮助,其他人对答案投了赞成票。我想你有权发表你的意见,尽管它可能很苛刻,但这种消极情绪往往会阻止人们提供帮助。
猜你喜欢
  • 2021-01-16
  • 1970-01-01
  • 1970-01-01
  • 2011-01-31
  • 2013-05-14
  • 2011-01-14
  • 2012-04-11
  • 2011-11-04
  • 1970-01-01
相关资源
最近更新 更多