SSE 字节和半字交换

rav*_*n02 2 c x86 sse simd intrinsics

我想使用 SSE 内在函数翻译此代码。

for (uint32_t i = 0; i < length; i += 4, src += 4, dest += 4)
{
    uint32_t value = *(uint32_t*)src;
    *(uint32_t*)dest = ((value >> 16) & 0xFFFF) | (value << 16);
}
Run Code Online (Sandbox Code Playgroud)

有没有人知道执行 16 位字交换的内在因素?

Pet*_*des 6

pshufb(SSSE3) 应该比 2 个班次和一个 OR 快。此外,对 shuffle 掩码的轻微修改将启用字节序转换,而不仅仅是单词交换。

窃取 Paul R 的函数结构,只替换向量内在函数:

void word_swapping_ssse3(uint32_t* dest, const uint32_t* src, size_t count)
{
    size_t i;
    __m128i shufmask =  _mm_set_epi8(13,12, 15,14,  9,8, 11,10,  5,4, 7,6,  1,0, 3,2);
    // _mm_set args go in big-endian order for some reason.                       

    for (i = 0; i + 4 <= count; i += 4)
    {
        __m128i s = _mm_loadu_si128((__m128i*)&src[i]);
        __m128i d = _mm_shuffle_epi8(s, shufmask);
        _mm_storeu_si128((__m128i*)&dest[i], d);
    }
    for ( ; i < count; ++i) // handle residual elements
    {
        uint32_t w = src[i];
        w = (w >> 16) | (w << 16);
        dest[i] = w;
    }
}
Run Code Online (Sandbox Code Playgroud)

pshufb可以有一个内存操作数,但它必须是混洗掩码,而不是要混洗的数据。所以你不能把它用作洗牌负载。:/

gcc 不会为循环生成很好的代码。主循环是

# src: r8.  dest: rcx.  count: rax.  shufmask: xmm1
.L16:
        movq    %r9, %rax
.L3:  # first-iteration entry point
        movdqu  (%r8), %xmm0
        leaq    4(%rax), %r9
        addq    $16, %r8
        addq    $16, %rcx
        pshufb  %xmm1, %xmm0
        movups  %xmm0, -16(%rcx)
        cmpq    %rdx, %r9
        jbe     .L16
Run Code Online (Sandbox Code Playgroud)

有了所有这些循环开销,并且需要单独的加载和存储指令,吞吐量将仅为每 2 个周期 1 次 shuffle。(8 uop,因为cmp宏与 融合jbe)。

更快的循环将是

  shl $2, %rax  # uint count  ->  byte count
  # check for %rax less than 16 and skip the vector loop
  # cmp / jsomething
  add %rax, %r8  # set up pointers to the end of the array
  add %rax, %rcx
  neg %rax       # and count upwards toward zero
.loop:
  movdqu (%r8, %rax), %xmm0
  pshufb  %xmm1, %xmm0
  movups  %xmm0, (%rcx, %rax)  # IDK why gcc chooses movups for stores.  Shorter encoding?
  add $16, %rax
  jl .loop
  # ...
  # scalar cleanup
Run Code Online (Sandbox Code Playgroud)

movdqu 与矢量 ALU 操作不同,负载可以与复杂的寻址模式进行微融合,因此我相信,除了存储之外,所有这些指令都是单微操作的。

这应该在每次迭代中运行 1 个周期并进行一些展开,因为add可以与jl. 所以循环总共有 5 个 uops。其中 3 个是加载/存储操作,具有专用端口。瓶颈是:pshufb只能在一个执行端口上运行(Haswell(SnB/IvB 可以pshufb在端口 1 和 5 上))。每个周期一个商店(所有微拱)。最后,英特尔 CPU 的每个时钟限制 4 个融合域 uop,除非 Nehalem 和更高版本(uop 循环缓冲区)上的缓存未命中,否则应该可以访问。

展开将使每 16B 的总融合域 uops 降至 4 以下。增量指针,而不是使用复杂的寻址模式,将使存储微熔断。(减少循环开销总是好的:让重新排序的缓冲区被未来的迭代填满意味着 CPU 在循环结束时遇到错误预测并返回到其他代码时有事情要做。)

这几乎是您通过展开内在循环所得到的,正如 Elalfer 正确建议的那样,这将是一个好主意。使用 gcc,尝试一下,-funroll-loops如果这不会使代码膨胀太多。

顺便说一句,在加载或存储时进行字节交换可能会更好,与其他代码混合,而不是将缓冲区转换为单独的操作。

  • 谢谢你的提醒。SSSE3 是与 Core2 一起引入的,现在已经快 10 年了,但是不检查就使用它仍然是不好的做法。(与 SSE2 不同,SSE2 在 amd64 中是非可选的。) (3认同)