使用 SIMD 对半字节的去交错向量

Posted

技术标签:

【中文标题】使用 SIMD 对半字节的去交错向量【英文标题】:Deinterleve vector of nibbles using SIMD 【发布时间】:2020-07-31 22:55:34 【问题描述】:

我有一个 16384 有符号四位整数的输入向量。它们被打包成 8192 字节。我需要将这些值交错并解压缩为两个单独数组中的有符号 8 位整数。

a,b,c,d 是 4 位值。 A,B,C,D 是 8 位值。

输入 = [ab,cd,...] Out_1 = [A,C, ...] Out_2 = [B,D, ...]

我可以在 C++ 中很容易地做到这一点。

constexpr size_t size = 32768;
int8_t input[size]; // raw packed 4bit integers
int8_t out_1[size];
int8_t out_2[size];

for (int i = 0; i < size; i++) 
    out_1[i] = input[i] << 4;
    out_1[i] = out_1[i] >> 4;
    out_2[i] = input[i] >> 4;

我想实现它以在通用处理器上尽可能快地运行。在 VOLK 中存在 8 位解交织到 16 位整数的良好 SIMD 实现,但我什至找不到基本的按字节 SIMD 移位运算符。

https://github.com/gnuradio/volk/blob/master/kernels/volk/volk_8ic_deinterleave_16i_x2.h#L63

谢谢!

【问题讨论】:

这可能很有趣:***.com/questions/44011366/avx-4-bit-integers x86 SIMD 没有字节移位,您必须通过 16 位或更宽的移位来模拟它们,并屏蔽掉进入每个字节顶部的位 (SSE/SIMD shift with one-byte element size / granularity?)。当您希望算术移位进行符号扩展时,这种情况很糟糕;也许设置高位的不同技巧可以用于固定移位计数。就像 xor0xf8 设置高位并翻转第 4 位,然后 paddb0x08 将更正第 4 位,或者执行并清除高位,或者保留它们。跨度> 等等,你的问题说你需要签名,但是你的 C++ 使用uint8_t 来处理所有事情,而不是int8_t。无符号更容易,只需移位和掩码。 (即使有字节移位,低半部分移位两次也是低效的;AND 与_mm_set1_epi8(0x0f) 用这个想法更新了SSE/SIMD shift with one-byte element size / granularity?:4 uop(对于英特尔)比以前模拟不存在的psrab (_mm_srai_epi8) 要好。 使用uint8_t input,您的out_2 结果仍然不正确。 (零扩展而不是符号扩展。)您可以将其设为int8_t*,或将其转换为((int8_t)input[i]) &gt;&gt; 4。这实际上是自动矢量化的,clang 相当好,GCC 相当差:godbolt.org/z/zYhff7 【参考方案1】:

这是一个例子。您的问题包含使用未签名操作的代码,但问题是关于签名的,所以我不确定您想要什么。如果它是你想要的无符号,只需删除实现符号扩展的位。

const __m128i mm_mask = _mm_set1_epi32(0x0F0F0F0F);
const __m128i mm_signed_max = _mm_set1_epi32(0x07070707);

for (size_t i = 0u, n = size / 16u; i < n; ++i)

    // Load and deinterleave input half-bytes
    __m128i mm_input_even = _mm_loadu_si128(reinterpret_cast< const __m128i* >(input) + i);
    __m128i mm_input_odd = _mm_srli_epi32(mm_input_even, 4);

    mm_input_even = _mm_and_si128(mm_input_even, mm_mask);
    mm_input_odd = _mm_and_si128(mm_input_odd, mm_mask);

    // If you need sign extension, you need the following
    // Get the sign bits
    __m128i mm_sign_even = _mm_cmpgt_epi8(mm_input_even, mm_signed_max);
    __m128i mm_sign_odd = _mm_cmpgt_epi8(mm_input_odd, mm_signed_max);

    // Combine sign bits with deinterleaved input
    mm_input_even = _mm_or_si128(mm_input_even, _mm_andnot_si128(mm_mask, mm_sign_even));
    mm_input_odd = _mm_or_si128(mm_input_odd, _mm_andnot_si128(mm_mask, mm_sign_odd));

    // Store the results
    _mm_storeu_si128(reinterpret_cast< __m128i* >(out_1) + i, mm_input_even);
    _mm_storeu_si128(reinterpret_cast< __m128i* >(out_2) + i, mm_input_odd);

如果您的 size 不是 16 的倍数,那么您还需要添加对尾字节的处理。您可以为此使用非矢量化代码。

请注意,在上面的代码中,您不需要按字节进行移位,因为无论如何您都必须应用掩码。因此,任何更粗粒度的转变都可以在这里进行。

【讨论】:

样式说明:我在您的 var 名称中发现 mm_ 使其难以阅读,到处都有太多的 mms。当我想区分名称相似的标量和向量时,我经常将v 作为变量名称的第一个字母,例如vsign。但在这里我只使用__m128i input_even = ... 请参阅SSE/SIMD shift with one-byte element size / granularity?,以使用pxor / paddbset1(0x08),在2 条指令中将低4 位符号扩展为一个字节的更有效方法。 (上面的 4 已经归零了)。应该比pcmpgtb / pandn / por 更有效率。嗯,尤其是如果我们将pxor before 分成高/低两半!我们可以在转移前pxorset1_epi8(0x88) 一些使用该技巧或 vpshufb 的版本,包括一个使用 clang 很好地自动矢量化的版本:godbolt.org/z/sGafhr。如果您关心 GCC,不幸的 GCC 错过了优化会使 pshufb 版本的吸引力降低(额外加载相同的数据)。将在某个时候作为答案发布。 @PeterCordes pxor/paddb 的技巧很好。我不认为在去交错之前移动pxor 会更快,因为它只会在依赖链中移动相同的延迟。稍后的两个pxors 将并行执行,因此它们实际上等同于解交错之前的pxor。但是pxor 的功耗可能会稍微少一些,而且内存肯定会更少。 在 Godbolt 上重新编码,注意你为 clang 启用了循环展开,但没有为 gcc 启用。另外,如果您发现 gcc 效率低下,请向上游报告 (gcc.gnu.org/bugzilla)。

以上是关于使用 SIMD 对半字节的去交错向量的主要内容,如果未能解决你的问题,请参考以下文章

使用 SIMD 指令去交错音频通道

Rust 获取 SIMD 向量中真实字节的索引

SIMD 零向量测试

Swift 中的向量 SIMD 类型

RGB 到 YCbCr 使用 SIMD 向量丢失一些数据

动态分配 SIMD 向量数组是不是安全?