如何在 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_*
还不存在。此外,该特定名称并不理想,因为 1
和 i
在视觉上并不是最明显的。 (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。
有许多类似衰变的物理事件(例如 体摩擦 或 电荷泄漏 ),通常在迭代器中建模,例如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_*
还不存在。此外,该特定名称并不理想,因为 1
和 i
在视觉上并不是最明显的。 (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。