如何在 sse 的有符号定点数学中实现向零衰减?

How to implement decay towards zero in signed fixed point math, in sse?

有许多类似衰变的物理事件(例如 体摩擦 电荷泄漏 ),通常在迭代器中建模,例如x' = x * 0.99,这在浮点运算中通常很容易写。

但是,我需要在 sse 中以 16 位“8.8”有符号定点方式执行此操作。为了在典型的 ALU 上有效实现,提到的公式可以重写为 x = x - x/128;x = x - (x>>7),其中 >> 是 "arithmetic",符号扩展右移。

我被困在这里,因为 _mm_sra_epi16() 产生了完全违反直觉的行为,这很容易通过以下示例验证:

#include <cstdint>
#include <iostream>
#include <emmintrin.h>

using namespace std;

int main(int argc, char** argv) {
    cout << "required: ";
    for (int i = -1; i < 7; ++i) {
        cout << hex << (0x7fff >> i) << ", ";
    }
    cout << endl;
    cout << "produced: ";
    __m128i a = _mm_set1_epi16(0x7fff);
    __m128i b = _mm_set_epi16(-1, 0, 1, 2, 3, 4, 5, 6);
    auto c = _mm_sra_epi16(a, b);
    for (auto i = 0; i < 8; ++i) {
        cout << hex << c.m128i_i16[i] << ", ";
    }
    cout << endl;
    return 0;
}

输出如下:

required: 0, 7fff, 3fff, 1fff, fff, 7ff, 3ff, 1ff,
produced: 0, 0, 0, 0, 0, 0, 0, 0,

它只对所有函数应用第一个 shift,就像它实际上是 _mm_sra1_epi16 函数一样,意外地命名为 sra 并且无缘无故地给了 __m128i 第二个参数 bu 一个有趣的子句。所以这不能在SSE中使用。

另一方面,我听说除法算法非常复杂,因此_mm_div_epi16在SSE中没有,也不能使用。 implement/vectorize 流行的 "decay" 技术该做什么以及如何使用?

x -= x>>7 使用 SSE2 实现很简单,使用恒定的移位计数来提高效率。如果 AVX 可用,这将编译为 2 条指令,否则在破坏性右移之前需要 movdqa 来复制 v

__m128i downscale(__m128i v){
    __m128i dec = _mm_srai_epi16(v, 7);
    return _mm_sub_epi16(v, dec);
}

GCC 甚至自动矢量化它 (Godbolt)。

void foo(short *__restrict a) {
    for (int i=0 ; i<10240 ; i++) {
        a[i] -= a[i]>>7;  // inner loop uses the same psraw / psubw
    }
}

float不同,定点在整个范围内具有恒定的绝对精度,而不是恒定的相对精度。因此对于较小的正数,v>>7 将为零并且您的减量将停止。 (负输入下溢到 -1,因为算术右移向-无穷大。)

如果移位可能下溢到 0 的小输入,您可能需要与 _mm_set1_epi16(1) 进行或操作以确保减量不为零。对大型输入的影响可以忽略不计。然而,这最终会使降级链从 0 变为 -1。 (然后返回到 0,因为 -1 | 1 == -1 在 2 的补码中)。

__m128i downscale_nonzero(__m128i v){
    __m128i dec = _mm_srai_epi16(v, 7);
    dec = _mm_or_si128(dec, _mm_set1_epi16(1));
    return _mm_sub_epi16(v, dec);
}

如果开始为负数,序列将是 -large,对数直到 -128,线性直到 -4, -3, -2, -1, 0, -1, 0, -1, ...


您的代码全为零,因为 _mm_sra_epi16 使用第二个源向量的低 64 位作为适用于所有元素的 64 位移位计数。 Read the manual。因此,您将每个 16 位元素的所有位都移出。

这不是白痴,但每个元素的移位计数需要 AVX2(用于 32/64 位元素)或 AVX512BW 用于 _mm_srav_epi16 或 64 位算术右移,这对您的方式有意义正在尝试使用它。 (但是移位计数是无符号的,所以 -1 也将移出所有位)。

Indeed, that instruction should be named _mm_sra1_epi16()

是的,这是有道理的。但请记住,当这些被命名时,AVX2 _mm_srav_* 还不存在。此外,该特定名称并不理想,因为 1i 在视觉上并不是最明显的。 (i 用于立即数,对于 psraw xmm1, imm16 形式而不是 asm 指令的 psraw xmm1, xmm2/m128 形式:http://felixcloutier.com/x86/PSRAW:PSRAD:PSRAQ.html)。

另一种有意义的方式是 MMX/SSE2 asm 指令有两种形式:立即数(当然所有元素的计数都相同)和向量。向量版本不是强制您将计数广播到所有元素,而是在向量寄存器的底部获取标量计数。我认为预期的用例是在 movd xmm0, eax 之后。


如果您需要在没有 AVX512 的情况下进行每个元素变量的移位计数,请参阅有关模拟它的各种问答,例如.

一些解决方法使用乘以 2 的幂进行可变左移,然后右移以将数据放在需要的地方。 (但是您需要以某种方式准备好 1<<n SIMD 向量,因此如果对许多向量重复使用同一组计数,或者特别是如果它是编译时常量,则此方法有效。

对于 16 位元素,您可以仅使用一个 _mm_mulhi_epi16 进行运行时变量右移计数,而不会丢失精度或范围限制。 mulhi(x*y)(x*(int)y) >> 16 完全相同,因此您可以使用 y=1<<14 在该元素中右移 16-14 = 2。