【问题标题】:Binary Interleaving, Binary Swizzling, Alternating Bits二进制交错、二进制混合、交替位
【发布时间】:2020-07-13 22:35:22
【问题描述】:

问题:

我有一系列索引位7 6 5 4 3 2 1 0,我想通过以下方式混合它们:

7 6 5 4 3 2 1 0  =  7 6 5 4 3 2 1 0
   _____| | | |     | | | |_____
  |    ___| | |     | | |___    |  
  |   |    _| |     | |_    |   |  
  |   |   |   |     |   |   |   |  
  v   v   v   v     v   v   v   v  
_ 3 _ 2 _ 1 _ 0     7 _ 6 _ 5 _ 4 _
       |___________________|
                 |
                 v
          7 3 6 2 5 1 4 0

即我想从一个字节中交错低半字节和高半字节的位。

天真的解决方案:

我可以使用以下方式在 C 中实现此行为:

int output = 
    ((input & (1 <<  0)) << 0) |
    ((input & (1 <<  1)) << 1) |
    ((input & (1 <<  2)) << 2) |
    ((input & (1 <<  3)) << 3) |
    ((input & (1 <<  4)) >> 3) |
    ((input & (1 <<  5)) >> 2) |
    ((input & (1 <<  6)) >> 1) |
    ((input & (1 <<  7)) >> 0);

但是它显然很笨重。

争取更优雅的解决方案:

我想知道是否有什么方法可以用更少的机器指令更快地实现这种行为。以 SSE 为例?

好奇的人的一些背景:

我使用它来将 2d 有符号整数向量坐标打包成 1d 值,在处理内存和缓存时可以保持邻近度。这个想法类似于移动设备上某些 GPU 使用的一些纹理布局优化。 (i ^ 0xAAAAAAAA) - 0xAAAAAAAA 将 1d 整数转换为 1d 有符号整数,使用我所说的两个邻近度的幂。 (x + 0xAAAAAAAA) ^ 0xAAAAAAAA 只是反向操作,从 1d 有符号整数到 1d 整数,仍然具有相同的属性。 为了让它变成 2d 并保持邻近属性,我想交替 x 和 y 位。

【问题讨论】:

  • 我可能会生成一个包含所有 256 个字符(覆盖整个值空间)的预混排查找表,例如char swiz[256]; for (int i = 0; i &lt; 256; i++) swiz[i] = swizzle(i);
  • @paddy 谢谢你的回答。我虽然关于那个选择。我仍然想知道是否没有一条指令可以避免自己在缓存中保存这个查找表的麻烦。
  • 你签出this了吗?它将两个 16 位值的位交错。据推测,您可以将该方法调整为 4 位值,尽管它可能效率低下。或者,也许你真的想在更广泛的数据上做这件事。
  • @paddy 哇,现在这很有趣!指令量基本上减半。我需要更好地研究它才能完全理解它......但是,如果它符合我的期望,那么它绝对是一个很好的解决方案。我会进行一些测试,看看它会导致什么。不,对于非常小的向量,我确实需要它,因为这与 GPU 的限制有关。然而,即使这种方法看起来也是可扩展的。非常感谢您富有洞察力的回答。
  • 也许可以看看上面那个interleaves two 8-bit values with 64-bit multiplication的方法。我认为您可以将其更改为将两个 4 位值与 32 位乘法相交。这可能非常适合您正在寻找的高效 SIMD 解决方案,例如 SSE。

标签: c binary bit-manipulation sse


【解决方案1】:

所以你想在每个字节中交错低半字节和高半字节的位?对于标量代码,最好使用 256 字节的查找表 (LUT)。

对于 x86 SIMD,SSSE3 pshufb (_mm_shuffle_epi8) 可用作 16x nibble->byte 并行查找的并行 LUT。使用它可以将一个半字节解压缩为一个字节。

__m128i interleave_high_low_nibbles(__m128i v) {
    const __m128i lut_unpack_bits_low = _mm_setr_epi8( 0, 1, 0b00000100, 0b00000101, 
              ...   // dcba -> 0d0c0b0a
     );
    const __m128i lut_unpack_bits_high = _mm_slli_epi32(lut_unpack_bits_low, 1);
                    // dcba -> d0c0b0a0

   // ANDing is required because pshufb uses the high bit to zero that element
   // 8-bit element shifts aren't available so also we have to mask after shifting
    __m128i lo = _mm_and_si128(v, _mm_set1_epi8(0x0f));
    __m128i hi = _mm_and_si128(_mm_srli_epi32(v, 4), _mm_set1_epi8(0x0f));

    lo = _mm_shuffle_epi8(lut_unpack_bits_low, lo);
    hi = _mm_shuffle_epi8(lut_unpack_bits_high, hi);
    return _mm_or_si128(lo, hi);
}

这并不比单个字节的内存 LUT 快,但 它可以并行处理 16 个字节pshufb 是过去十年制造的 x86 CPU 上的单指令。 (在第一代 Core 2 和 K8 上速度较慢。)

拥有单独的 lo/hi LUT 矢量意味着可以将设置提升到循环之外;否则我们需要在 ORing 之前移动一个 LUT 结果。

【讨论】:

  • 完美,这就是我一直在寻找的东西。我实际上正在使用 16 位整数,8 位是为了让问题保持简单。这并不重要,因为这个答案很容易适应。
  • @Mendear:酷。请注意,如果您有 AVX2,则可以使用_mm256_shuffle_epi8 一次对两个 128 位通道执行此操作。或者使用 AVX512BW for vpermw,您可以使用 16 位字的 LUT 而不是 8 位字节来执行 64 字节块。此外,SSE2 确实有单词移位,因此您可能可以保存一些 ANDing,例如AND 和 ANDNOT 与 set1_epi16(0x0f0f) 然后使用 epi16 逻辑右移 4、8 和 12。(并且 &gt;&gt;12 的源操作数不需要先被屏蔽)
  • @Mendear:在我上一条评论的结尾处放屁:您可能仍然希望为每个 pshufb 使用每个字节,因此移位 12 不会有用。如果您想使用 16 位交错发布答案或编辑我的答案,这可能对未来的读者有用。
猜你喜欢
  • 2016-09-21
  • 2017-10-27
  • 1970-01-01
  • 2021-08-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-12-21
  • 1970-01-01
相关资源
最近更新 更多