使用双精度运算的快速 SSE 低精度指数
Fast SSE low precision exponential using double precision operations
我正在寻找快速 SSE 低精度 (~1e-3) 指数函数。
我遇到了这个很棒的东西:
/* max. rel. error = 3.55959567e-2 on [-87.33654, 88.72283] */
__m128 FastExpSse (__m128 x)
{
__m128 a = _mm_set1_ps (12102203.0f); /* (1 << 23) / log(2) */
__m128i b = _mm_set1_epi32 (127 * (1 << 23) - 298765);
__m128i t = _mm_add_epi32 (_mm_cvtps_epi32 (_mm_mul_ps (a, x)), b);
return _mm_castsi128_ps (t);
}
基于 Nicol N. Schraudolph 的工作:N. N. Schraudolph。 "A fast, compact approximation of the exponential function." 神经计算,11(4),1999 年 5 月,pp.853-862。
现在我需要一个 "double precision" 版本:__m128d FastExpSSE (__m128d x)
。
这是因为我没有控制输入输出精度,刚好是double精度,两次转换double -> float,然后float -> double就吃掉了50%的CPU资源
需要进行哪些更改?
我天真地试过这个:
__m128i double_to_uint64(__m128d x) {
x = _mm_add_pd(x, _mm_set1_pd(0x0010000000000000));
return _mm_xor_si128(
_mm_castpd_si128(x),
_mm_castpd_si128(_mm_set1_pd(0x0010000000000000))
);
}
__m128d FastExpSseDouble(__m128d x) {
#define S 52
#define C (1llu << S) / log(2)
__m128d a = _mm_set1_pd(C); /* (1 << 52) / log(2) */
__m128i b = _mm_set1_epi64x(127 * (1llu << S) - 298765llu << 29);
auto y = double_to_uint64(_mm_mul_pd(a, x));
__m128i t = _mm_add_epi64(y, b);
return _mm_castsi128_pd(t);
}
当然这个 returns 垃圾因为我不知道我在做什么...
编辑:
关于 50% 的因素,这是一个非常粗略的估计,比较加速(相对于 std::exp)将单精度数字向量(很好)转换为具有双精度列表的加速数字(不太好)。
这是我使用的代码:
// gives the result in place
void FastExpSseVector(std::vector<double> & v) { //vector with several millions elements
const auto I = v.size();
const auto N = (I / 4) * 4;
for (int n = 0; n < N; n += 4) {
float a[4] = { float(v[n]), float(v[n + 1]), float(v[n + 2]), float(v[n + 3]) };
__m128 x;
x = _mm_load_ps(a);
auto r = FastExpSse(x);
_mm_store_ps(a, r);
v[n] = a[0];
v[n + 1] = a[1];
v[n + 2] = a[2];
v[n + 3] = a[3];
}
for (int n = N; n < I; ++n) {
v[n] = FastExp(v[n]);
}
}
如果我有这个 "double precision" 版本,我会这样做:
void FastExpSseVectorDouble(std::vector<double> & v) {
const auto I = v.size();
const auto N = (I / 2) * 2;
for (int n = 0; n < N; n += 2) {
__m128d x;
x = _mm_load_pd(&v[n]);
auto r = FastExpSseDouble(x);
_mm_store_pd(&v[n], r);
}
for (int n = N; n < I; ++n) {
v[n] = FastExp(v[n]);
}
}
像这样的东西应该可以完成工作。您需要调整 1.05
常量以获得较低的最大误差——我懒得这样做:
__m128d fastexp(const __m128d &x)
{
__m128d scaled = _mm_add_pd(_mm_mul_pd(x, _mm_set1_pd(1.0/std::log(2.0)) ), _mm_set1_pd(3*1024.0-1.05));
return _mm_castsi128_pd(_mm_slli_epi64(_mm_castpd_si128(scaled), 11));
}
这仅获得了大约 2.5% 的相对精度——为了获得更高的精度,您可能需要添加第二项。
此外,对于上溢或下溢的值,这将导致未指定的值,您可以通过将 scaled
值限制为某些值来避免这种情况。
我正在寻找快速 SSE 低精度 (~1e-3) 指数函数。
我遇到了这个很棒的东西
/* max. rel. error = 3.55959567e-2 on [-87.33654, 88.72283] */
__m128 FastExpSse (__m128 x)
{
__m128 a = _mm_set1_ps (12102203.0f); /* (1 << 23) / log(2) */
__m128i b = _mm_set1_epi32 (127 * (1 << 23) - 298765);
__m128i t = _mm_add_epi32 (_mm_cvtps_epi32 (_mm_mul_ps (a, x)), b);
return _mm_castsi128_ps (t);
}
基于 Nicol N. Schraudolph 的工作:N. N. Schraudolph。 "A fast, compact approximation of the exponential function." 神经计算,11(4),1999 年 5 月,pp.853-862。
现在我需要一个 "double precision" 版本:__m128d FastExpSSE (__m128d x)
。
这是因为我没有控制输入输出精度,刚好是double精度,两次转换double -> float,然后float -> double就吃掉了50%的CPU资源
需要进行哪些更改?
我天真地试过这个:
__m128i double_to_uint64(__m128d x) {
x = _mm_add_pd(x, _mm_set1_pd(0x0010000000000000));
return _mm_xor_si128(
_mm_castpd_si128(x),
_mm_castpd_si128(_mm_set1_pd(0x0010000000000000))
);
}
__m128d FastExpSseDouble(__m128d x) {
#define S 52
#define C (1llu << S) / log(2)
__m128d a = _mm_set1_pd(C); /* (1 << 52) / log(2) */
__m128i b = _mm_set1_epi64x(127 * (1llu << S) - 298765llu << 29);
auto y = double_to_uint64(_mm_mul_pd(a, x));
__m128i t = _mm_add_epi64(y, b);
return _mm_castsi128_pd(t);
}
当然这个 returns 垃圾因为我不知道我在做什么...
编辑:
关于 50% 的因素,这是一个非常粗略的估计,比较加速(相对于 std::exp)将单精度数字向量(很好)转换为具有双精度列表的加速数字(不太好)。
这是我使用的代码:
// gives the result in place
void FastExpSseVector(std::vector<double> & v) { //vector with several millions elements
const auto I = v.size();
const auto N = (I / 4) * 4;
for (int n = 0; n < N; n += 4) {
float a[4] = { float(v[n]), float(v[n + 1]), float(v[n + 2]), float(v[n + 3]) };
__m128 x;
x = _mm_load_ps(a);
auto r = FastExpSse(x);
_mm_store_ps(a, r);
v[n] = a[0];
v[n + 1] = a[1];
v[n + 2] = a[2];
v[n + 3] = a[3];
}
for (int n = N; n < I; ++n) {
v[n] = FastExp(v[n]);
}
}
如果我有这个 "double precision" 版本,我会这样做:
void FastExpSseVectorDouble(std::vector<double> & v) {
const auto I = v.size();
const auto N = (I / 2) * 2;
for (int n = 0; n < N; n += 2) {
__m128d x;
x = _mm_load_pd(&v[n]);
auto r = FastExpSseDouble(x);
_mm_store_pd(&v[n], r);
}
for (int n = N; n < I; ++n) {
v[n] = FastExp(v[n]);
}
}
像这样的东西应该可以完成工作。您需要调整 1.05
常量以获得较低的最大误差——我懒得这样做:
__m128d fastexp(const __m128d &x)
{
__m128d scaled = _mm_add_pd(_mm_mul_pd(x, _mm_set1_pd(1.0/std::log(2.0)) ), _mm_set1_pd(3*1024.0-1.05));
return _mm_castsi128_pd(_mm_slli_epi64(_mm_castpd_si128(scaled), 11));
}
这仅获得了大约 2.5% 的相对精度——为了获得更高的精度,您可能需要添加第二项。
此外,对于上溢或下溢的值,这将导致未指定的值,您可以通过将 scaled
值限制为某些值来避免这种情况。