【问题标题】:SSE byte and half word swappingSSE 字节和半字交换
【发布时间】:2015-07-03 09:57:53
【问题描述】:

我想使用 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 位字交换的内在函数吗?

【问题讨论】:

  • 为什么不直接编写可重用代码并针对目标平台进行优化?你用的是什么编译器?
  • 这并不能算作 IMO 的尝试,至少要加入一些内在函数

标签: c x86 sse simd intrinsics


【解决方案1】:

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

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

有了所有循环开销,并且需要单独的加载和存储指令,吞吐量将仅为每 2 个周期 1 次随机播放。 (8 微指令,因为 cmpjbe 宏融合)。

更快的循环是

  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

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

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

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

正如 Elalfer 正确建议的那样,这几乎就是展开内在循环所得到的结果。使用 gcc,如果代码不会过于臃肿,请尝试 -funroll-loops

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

【讨论】:

  • 很好 - 值得一提的是 pshufb 需要 SSSE3。
  • 感谢您的提醒。 SSSE3 是与 Core2 一起引入的,现在已经快 10 年了,但是不检查就使用它仍然是不好的做法。 (与 SSE2 不同,后者在 amd64 中是非可选的。)
  • 确实——而且我似乎记得 AMD 采用 SSSE3(和 SSE4)很晚——我已经很长时间没有真正关注 AMD CPU,但我认为他们现在已经赶上了?
  • 哇,你说得对。直到 Bulldozer (2011)(AMD 赶上了 AVX)。主要是因为英特尔不会很早就发布细节,或者在最后一刻做出改变。因此,AMD 必须在很短的时间内开始设计它,然后英特尔才能出售最终的芯片。 AMD 在 Excavator (2015) 之前不会拥有 AVX2 CPU。
  • AMD 在 2005 年经历了一个艰难的时期,因为他们在早期的 AMD64 上失去了很多领先于英特尔的优势——出于某种原因,他们决定跳过 SSSE3 和 SSE4,转而采用他们所谓的 SSE5——但是课程兼容性为王,修复这些错误并(几乎)进入公平竞争环境需要很长时间。
【解决方案2】:

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

void byte_swapping(uint32_t* dest, const uint32_t* src, size_t count)
{
    size_t i;
    for (i = 0; i + 4 <= count; i += 4)
    {
        __m128i s = _mm_loadu_si128((__m128i*)&src[i]);
        __m128i d = _mm_or_si128(_mm_slli_epi32(s, 16), _mm_srli_epi32(s, 16));
        _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;
    }
}

【讨论】:

  • 是的,你是对的。这是另一个字节序转换。谢谢你的帮助。
  • 嘿,我刚刚看了这个。 gcc 4.9.2 amd64 自动向量化清理循环并生成大量 int 代码,以及向量循环的另一个副本。 /facepalm。
  • 我怀疑pshufb 会比 2 班次 + 一个或更有效。
  • 你们太棒了。我们基本上是 AVX 目标并运行整个 xbox 360 模拟器,强制要求 openGL 4.5。
  • 这实际上是一个 16 的 32 位 ror,在 AVX512 规范中有 _mm_ror_epi32。再等一年;)性能方面,在大多数架构上使用pshufb2*shift 应该更好,但在英特尔有 2 个移位端口的情况下可能是一样的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-08-20
  • 2018-07-10
  • 2013-04-05
  • 2017-05-20
  • 2018-10-15
相关资源
最近更新 更多