【问题标题】:SIMD: Bit-pack signed integersSIMD:位包有符号整数
【发布时间】:2021-07-09 07:15:21
【问题描述】:

可以使用“位打包”技术来压缩无符号整数:在一个无符号整数块中,仅存储有效位,当一个块中的所有整数都是“小”时,会导致数据压缩。该方法被称为FOR(参考框架)。

有 SIMD libraries 可以非常有效地执行此操作。

现在我想使用类似 FOR 的技术来编码 signed 整数,例如来自未排序的无符号整数的不同序列。每个有符号整数的符号需要存储在某个地方,有两种选择:

  1. 将符号存储在单独的数据块中。这会增加开销。
  2. 将符号与每个有符号整数的绝对值一起存储。

我现在正在遵循路径 2。 2-s 补码在 msb(最高有效位)中有符号位,因此这不适用于位打包 à la FOR。一种可能性是将符号存储在 lsb(最低有效位)中。以这种方式存储有符号整数是非常不寻常的,据我所知,没有支持这一点的指令。现在的问题是:这些 lsb-signed-integer 能否使用 SIMD 指令高效地编码/解码?

我认为 AVX-512 _mm_testn_epi32_mask 可用于从每个 uint32 中提取 lsb,然后是移位,然后是某种形式的两个 mask_extract?相当复杂。

【问题讨论】:

  • 从有符号整数到无符号整数的常见且可逆的映射是:(x < 0) ? (-2 * x - 1) : (2 * x)。你能利用它吗?
  • AVX-512 具有 32 位元素的 SIMD 循环,如果有帮助的话。 (felixcloutier.com/x86/vprold:vprolvd:vprolq:vprolvq)。所以你可以把符号位放到底部。但是我想你会想要掩盖所有其他领先的负数。也许您可以对负输入进行位翻转或取反,因此以前导零结尾,并且可以在解码时将低位用作是否取反的信号。 (与x ^ -1 不同,旋转后的0-x 的优势在于将低位设置为负数。)我认为这与@njuffa 建议的映射相同。
  • 是的,所以使用vprold zmm, zmm, 1 / vpabsd zmm, zmm(32 位元素的绝对值)进行编码。解码...当我想到某些事情时,如果没有人击败我,我会发布答案:P
  • 10年前问过一个相关问题:stackoverflow.com/questions/3557599/…
  • 实现@njuffa的建议,注意-x - 1~x一样,所以(x < 0) ? (-2 * x - 1) : (2 * x)(x>>31) ^ (x+x)一样。

标签: sse simd avx avx2 avx512


【解决方案1】:

在 C 中使用 SSE2 处理 64 位整数的未经测试的 ZigZag 示例:

(注意:SSE2 缺少一些 64 位指令...)

#include <emmintrin.h>


// from comment by Peter-Cordes 
__m128i zigzag_encode_epi64(__m128i v) {
    __m128i signmask = _mm_shuffle_epi32(v, _MM_SHUFFLE(3,3,1,1));
    signmask = _mm_srai_epi32(signmask, 31);
    return _mm_xor_si128(_mm_add_epi64(v, v), signmask);
}


__m128i zigzag_decode_epi64 (__m128i v) {
    __m128i signmask = _mm_and_si128(_mm_set_epi32(0, 1, 0, 1), v);
    signmask = _mm_sub_epi64(_mm_setzero_si128(), signmask);
    return _mm_xor_si128(_mm_srli_epi64(v, 1), signmask);
}


// no constant
__m128i zigzag_decodev3_epi64 (__m128i v) {
    __m128i t = _mm_srli_epi64(v, 1);
    __m128i signmask = _mm_sub_epi64(_mm_slli_epi64(t, 1), v);
    return _mm_xor_si128(t, signmask);
}

Zigzag 适用于按位变量。但是,按字节分组的变量可能希望“符号从可变位宽扩展”。


32 位示例

我喜欢比较而不是算术移位。我假设 - 展开时 - 比较的延迟会降低 1 个周期。

__m128i zigzag_encode_epi32 (__m128i v) {
    __m128i signmask =_mm_cmpgt_epi32(_mm_setzero_si128(), v);
    return _mm_xor_si128(_mm_add_epi32(v, v), signmask);
}


__m128i zigzag_decode_epi32 (__m128i v) {
    const __m128i m = _mm_set1_epi32(1);
    __m128i signmask =_mm_cmpeq_epi32(_mm_and_si128(m, v), m);
    return _mm_xor_si128(_mm_srli_epi32(v, 1), signmask);
}


__m128i delta_encode_epi32 (__m128i v, __m128i prev) {
    return _mm_sub_epi32(v, _mm_alignr_epi8(v, prev, 12));
}


// prefix sum (see many of answers around stackoverflow...)
__m128i delta_decode_epi32 (__m128i v, __m128i prev) {
    prev = _mm_shuffle_epi32(prev, _MM_SHUFFLE(3,3,3,3)); // [P P P P]
    v = _mm_add_epi32(v, _mm_slli_si128(v, 4)); // [A AB BC CD]
    prev = _mm_add_epi32(prev, v); // [PA PAB PBC PCD]
    v = _mm_slli_si128(v, 8); // [0 0 A AB]
    return _mm_add_epi32(prev, v); // [PA PAB PABC PABCD]
}


__m128i delta_zigzag_encode_epi32 (__m128i v, __m128i prev) {
    return zigzag_encode_epi32(delta_encode_epi32(v, prev));
}


__m128i delta_zigzag_decode_epi32 (__m128i v, __m128i prev) {
    return delta_decode_epi32(zigzag_decode_epi32(v), prev);
}

注意:Delta 编码会更快(往返/解码)在编码时转置元素,然后在解码期间再次转置它们;水平前缀总和真的很慢。然而,确定每批中要转置的最佳元素数量似乎是一个难题。

【讨论】:

  • 如果您将不存在的psraq 模拟为pshufd + psrad,则可能避免需要归零寄存器(和移动)进行编码。 godbolt.org/z/xjd9Kozjh 具有 SSE4 解码和不需要常量的备用 SSE2 解码。 (当你计算被复制和洗牌取代的 movdqa 时,更多的换档端口压力,但总前端 uops 更少。)
  • 请注意,当 AVX 或 SSE4 可用时,clang 将您的原始编码优化为比这更好的 asm。因此,也许值得编写一个手动 SSE4.2 pcmpgtq 版本,所有编译器都可以在可用时使用,即使是在优化 SIMD 内在函数方面不如 clang 的编译器。请注意,OP 甚至有 AVX-512 可用,因此您甚至可以使用算术 qword 右移,或 test-into-mask 用于合并屏蔽的 sub-from-0。
  • 我假设 - 展开时 - 比较的延迟会降低 1 个周期。 - 为什么?在 Agner Fog 除了 AMD K10 之外的所有微架构上,像 psrad 这样的即时计数转换与 pandpcmpgtd 具有相同的延迟(通常为 1c)。您是否正在查看 psrad xmm, xmm 的延迟,这在某些(例如 Bulldozer)上是一个较慢的周期?
  • 或者你的意思是因为它需要 movdqa + psrad,而不是关键路径的异或归零?这是 CPU 太旧而无法消除 XMM mov 的一个因素(最初出现在 IvyBridge 和 Bulldozer 中)。但是,尽管您选择了内在函数,GCC 和 clang 都将比较优化为 movdqa + psrad xmm, 31godbolt.org/z/W5orrojWb。 Clang 甚至将== 1 比较变成v &lt;&lt;31 &gt;&gt;31。 GCC 愚蠢地重复了同一个常数两次。运气好的话,链接器会合并它:/
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2015-08-12
  • 2013-01-13
  • 1970-01-01
  • 1970-01-01
  • 2015-07-28
  • 2012-02-11
相关资源
最近更新 更多