SSE字节和半字交换

时间:2015-07-03 09:57:54

标签: 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);
}

是否有人知道执行16位字交换的固有内容?

2 个答案:

答案 0 :(得分:3)

pshufb(SSSE3)应该比2班和OR更快。此外,稍微修改shuffle掩码将启用endian转换,而不仅仅是单词交换。

窃取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;
    }
}

pshufb可以有一个内存操作数,但它必须是shuffle掩码,而不是要被洗牌的数据。因此,您无法将其用作混乱负载。 :/

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

由于所有这些循环开销,并且需要单独的加载和存储指令,吞吐量每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
与矢量ALU操作不同,

movdqu加载可以与复杂的寻址模式进行微融合,所以除了商店之外,所有这些指令都是单uop。我知道。

这应该在每次迭代时以1个周期运行并进行一些展开,因为add可以与jl微融合。所以循环有5个uop。其中3个是加载/存储操作,具有专用端口。瓶颈是:pshufb只能在一个执行端口上运行(Haswell(端口1和5上的SnB / IvB可以pshufb))。每个周期一个商店(所有微观)。最后,对于Intel CPU,每个时钟限制的4个融合域uop,应该可以在Nehalem和更高版本(uop循环缓冲区)上禁止缓存丢失。

展开会使每16B的融合域uop总数低于4.增加指针,而不是使用复杂的寻址模式,会让商店微融合。 (减少循环开销总是很好:让重新排序缓冲区填满未来的迭代意味着CPU在循环结束时遇到错误预测并返回到其他代码时有事情要做。)

这正是你通过展开内在函数循环得到的,正如Elalfer正确地建议的那样是个好主意。使用gcc,如果代码没有过多膨胀,请尝试-funroll-loops

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

答案 1 :(得分:2)

你问题中的标量代码实际上不是字节交换(至少在字节序转换的意义上) - 它只是在32位字内交换高16位和低16位。如果这是您想要的,那么只需重新使用the solution to your previous question,并进行适当的更改:

public