C ++中非常快速的近似Logarithm(自然日志)函数?

问题描述 投票:6回答:4

我们找到了各种技巧来取代std::sqrtTiming Square Root)和一些替换std::expUsing Faster Exponential Approximation),但我找不到任何替代std::log

它是我程序中循环的一部分,它被多次调用,而exp和sqrt被优化,英特尔VTune现在建议我优化std::log,之后似乎只有我的设计选择才会受到限制。

现在我在ln(1+x)x之间使用-0.5的qalexswpoi的三阶泰勒逼近(最大误差为4%的情况下为90%),否则回落到+0.5。这让我加速了15%。

c++ math logarithm micro-optimization sqrt
4个回答
9
投票

在开始设计和部署针对性能的超越函数的定制实现之前,强烈建议在算法级别以及通过工具链进行优化。不幸的是,我们没有关于要在此优化的代码的任何信息,也没有关于工具链的信息。

在算法级别,检查是否真正需要所有对超越函数的调用。也许存在需要较少函数调用的数学变换,或者将超越函数转换为代数运算。任何超越函数调用都可能是多余的,例如因为计算是不必要地进出对数空间?如果精度要求适中,整个计算能否以单精度执行,使用std::log而不是float?在大多数硬件平台上,避免double计算可以显着提高性能。

编译器倾向于提供各种影响数字密集型代码性能的开关。除了将常规优化级别增加到double之外,通常还有一种方法可以关闭非正常支持,即打开flush-to-zero或FTZ模式。这在各种硬件平台上具有性能优势。此外,通常还有一个“快速数学”标志,其使用会导致精度略有降低,并消除处理特殊情况(如NaN和无穷大)的开销,以及-O3的处理。一些编译器还支持代码的自动矢量化,并附带SIMD数学库,例如英特尔编译器。

对数函数的自定义实现通常涉及将二进制浮点参数errno分离为指数x和尾数e,使得mx = m * 2,因此e。选择log(x) = log(2) * e + log(m)使其接近统一,因为这提供了有效的近似,例如mlog(m) = log(1+f) = log1p(f)

C ++提供了minimax polynomial approximation函数来将浮点操作数分隔成尾数和指数,但实际上,通常使用更快的机器特定方法,通过将它们重新解释为相同大小的整数来操作位级别的浮点数据。下面的单精度对数代码frexp()演示了这两种变体。函数logf()__int_as_float()__float_as_int()重新解释为IEEE-754 int32_t浮点数,反之亦然。此代码严重依赖于大多数当前处理器,CPU或GPU上的硬件中直接支持的融合乘法 - 加法运算FMA。在binary32映射到软件仿真的平台上,此代码将会慢得令人无法接受。

fmaf()

如代码注释中所述,上面的实现提供了忠实的单精度结果,并且它处理符合IEEE-754浮点标准的特殊情况。通过消除特殊情况支持,消除对非正规参数的支持以及降低准确性,可以进一步提高性能。这导致以下示例性变体:

#include <cmath>
#include <cstdint>

/* compute natural logarithm, maximum error 0.85756 ulps */
float my_logf (float a)
{
    float m, r, s, t, i, f;
    int32_t e;

    if ((a > 0.0f) && (a <= 3.40282347e+38f)) { // 0x1.fffffep+127
#if PORTABLE
        m = frexpf (a, &e);
        if (m < 0.666666667f) {
            m = m + m;
            e = e - 1;
        }
        i = (float)e;
#else // PORTABLE
        i = 0.0f;
        /* fix up denormal inputs */
        if (a < 1.175494351e-38f){ // 0x1.0p-126
            a = a * 8388608.0f; // 0x1.0p+23
            i = -23.0f;
        }
        e = (__float_as_int (a) - 0x3f2aaaab) & 0xff800000;
        m = __int_as_float (__float_as_int (a) - e);
        i = fmaf ((float)e, 1.19209290e-7f, i); // 0x1.0p-23
#endif // PORTABLE
        /* m in [2/3, 4/3] */
        f = m - 1.0f;
        s = f * f;
        /* Compute log1p(f) for f in [-1/3, 1/3] */
        r = fmaf (-0.130187988f, f, 0.140889585f); // -0x1.0aa000p-3, 0x1.208ab8p-3
        t = fmaf (-0.121489584f, f, 0.139809534f); // -0x1.f19f10p-4, 0x1.1e5476p-3
        r = fmaf (r, s, t);
        r = fmaf (r, f, -0.166845024f); // -0x1.55b2d8p-3
        r = fmaf (r, f,  0.200121149f); //  0x1.99d91ep-3
        r = fmaf (r, f, -0.249996364f); // -0x1.fffe18p-3
        r = fmaf (r, f,  0.333331943f); //  0x1.5554f8p-2
        r = fmaf (r, f, -0.500000000f); // -0x1.000000p-1
        r = fmaf (r, s, f);
        r = fmaf (i, 0.693147182f, r); //   0x1.62e430p-1 // log(2) 
    } else {
        r = a + a;  // silence NaNs if necessary
        if (a  < 0.0f) r =  0.0f / 0.0f; //  NaN
        if (a == 0.0f) r = -1.0f / 0.0f; // -Inf
    }
    return r;
}

3
投票

看看/* natural log on [0x1.f7a5ecp-127, 0x1.fffffep127]. Maximum relative error 9.4529e-5 */ float my_faster_logf (float a) { float m, r, s, t, i, f; int32_t e; e = (__float_as_int (a) - 0x3f2aaaab) & 0xff800000; m = __int_as_float (__float_as_int (a) - e); i = (float)e * 1.19209290e-7f; // 0x1.0p-23 /* m in [2/3, 4/3] */ f = m - 1.0f; s = f * f; /* Compute log1p(f) for f in [-1/3, 1/3] */ r = fmaf (0.230836749f, f, -0.279208571f); // 0x1.d8c0f0p-3, -0x1.1de8dap-2 t = fmaf (0.331826031f, f, -0.498910338f); // 0x1.53ca34p-2, -0x1.fee25ap-2 r = fmaf (r, s, t); r = fmaf (r, s, f); r = fmaf (i, 0.693147182f, r); // 0x1.62e430p-1 // log(2) return r; } 的讨论,接受的答案是指基于Zeckendorf分解计算对数函数的this

在实现文件的评论中,讨论了复杂性和一些达到O(1)的技巧。

希望这可以帮助!


0
投票

-3
投票

这取决于你需要多准确。通常会调用log来了解数字的大小,通过检查浮点数的指数字段,您可以基本上免费进行。这也是你的第一个近似值。我将为我的书“Basic Algorithms”添加一个插件,它解释了如何从第一原理实现标准库数学函数。

© www.soinside.com 2019 - 2024. All rights reserved.