【发布时间】:2018-11-14 19:12:14
【问题描述】:
我正在寻找快速 SSE 低精度 (~1e-3) 指数函数。
我遇到了这个很棒的answer:
/* 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。 “指数函数的快速、紧凑的近似。”神经计算,11(4),1999 年 5 月,第 853-862 页。
现在我需要一个“双精度”版本:__m128d FastExpSSE (__m128d x)。
这是因为我不控制输入输出精度,正好是双精度,两次转换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);
}
当然这会返回垃圾,因为我不知道自己在做什么......
编辑:
关于 50% 的因素,这是一个非常粗略的估计,比较了将单精度数字向量(很好)转换为双精度数字列表(不是太棒了)。
这是我使用的代码:
// 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]);
}
}
如果我有这个“双精度”版本,我会这样做:
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]);
}
}
【问题讨论】:
-
您如何测量 double->float 和 float->double 占用了 50% 的 CPU 时间?你不是用单独的加载/存储/转换循环做那些吗?那么,您是否使用分析器并发现
cvtpd2ps+cvtps2pd指令具有 50% 的时钟周期事件样本,用于FastExpSse(__m128d)进行动态转换? (不过,这并不是高效的!与pack/unpack不同,你会得到一个 2floats 的向量,所以这将是纯粹的开销。) -
反正
127是en.wikipedia.org/wiki/Single-precision_floating-point_format中的指数偏差,它使用8位指数。我不确定298765来自什么。如果您还没有,您应该对 Nic 对单精度问题的回答发表评论,并附上该问题的链接。 (写这篇论文的人是 SO 用户 :) -
@PeterCordes 谢谢!关于 50% 的因素,请参阅我的编辑。
-
难怪像那样转换会很慢! gcc7.3
-O3godbolt.org/g/G1MGs9 确实设法对 double->float 进行矢量化,并避免在该步骤中实际通过内存中的a[]。但是 float -> double 步骤使用 4 个单独的标量 float->double 转换指令。 (不过,它确实设法避免在内存中弹跳。)显而易见的方法是使用_mm_cvtpd_ps(felixcloutier.com/x86/CVTPD2PS.html) 两次并在上面的 2 个元素中提供带有垃圾的向量FastExpSse。或转换 x2/unpcklpd/FastExpSse/convert/unpckhpd/convert。 -
我的错,我实际上使用的是完全优化的 icc 17。我正在为 SSE2 构建。
标签: c++ precision sse simd exponential