【问题标题】:Fast SSE low precision exponential using double precision operations使用双精度运算的快速 SSE 低精度指数
【发布时间】: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 不同,你会得到一个 2 floats 的向量,所以这将是纯粹的开销。)
  • 反正127en.wikipedia.org/wiki/Single-precision_floating-point_format中的指数偏差,它使用8位指数。我不确定298765 来自什么。如果您还没有,您应该对 Nic 对单精度问题的回答发表评论,并附上该问题的链接。 (写这篇论文的人是 SO 用户 :)
  • @PeterCordes 谢谢!关于 50% 的因素,请参阅我的编辑。
  • 难怪像那样转换会很慢! gcc7.3 -O3 godbolt.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


【解决方案1】:

这样的事情应该可以完成。您需要调整 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 值限制为某些值来避免这种情况。

【讨论】:

  • 哇,太棒了,谢谢!将此与使用联合的原始 FastExp 函数进行基准比较,它“仅”快 20%。我做错了吗?
  • (我正在使用 MSVC 进行测试,我将使用 icc 17 进行测试)
  • 做一个高斯模糊比计算exp要多得多,可以加入到这个计算中。但这将是一个不同的问题。
  • @ThreeStarProgrammer57:这可能值得将double 的两个向量转换成float 的一个向量,这与exp 不同。转换结果被打包到__m128 (felixcloutier.com/x86/CVTPD2PS.html) 的低 2 个float 元素中,所以就像我说的,您可以将它们与shufpdunpcklpdmovlhps 一起洗牌。这将每 4 个元素添加 2x 转换 + 1x shuffle + 2x 转换,但是通过每个向量的元素数量增加一倍,将其余工作减半。 (或者更好,如果使用 float 可以更有效地完成 FastExpSSE)
  • @PeterCordes 没错,但事情是为了进行高斯模糊,你几乎不需要大量的exp 计算。您可以预先计算一维查找表并使用exp(a+b)==exp(a)*exp(b) 身份——假设dx, dy, dz 是离散的。或者如果卷积核比较大,可以考虑做一个 3D-FastFourierTransform (但这离原来的问题有点远了)。
猜你喜欢
  • 2017-10-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-09-24
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多