关于log(x)的更快近似


10

不久前,我编写了一个代码,试图在不使用库函数的情况下计算。昨天,我正在查看旧代码,并尝试使其尽快(正确)。到目前为止,这是我的尝试:log(x)

const double ee = exp(1);

double series_ln_taylor(double n){ /* n = e^a * b, where a is an non-negative integer */
    double lgVal = 0, term, now;
    int i, flag = 1;

    if ( n <= 0 ) return 1e-300;
    if ( n * ee < 1 )
        n = 1.0 / n, flag = -1; /* for extremely small n, use e^-x = 1/n */

    for ( term = 1; term < n ; term *= ee, lgVal++ );
    n /= term;

    /* log(1 - x) = -x - x**2/2 - x**3/3... */
    n = 1 - n;
    now = term = n;
    for ( i = 1 ; ; ){
        lgVal -= now;
        term *= n;
        now = term / ++i;
        if ( now < 1e-17 ) break;
    }

    if ( flag == -1 ) lgVal = -lgVal;

    return lgVal;
}

在这里,我试图找到使e a刚好超过n的值,然后将n的对数值相加aeanealog(1  x)

最近,我对数值分析产生了兴趣,这就是为什么我不禁要问这个问题:在足够正确的情况下,该代码段可以在实践中运行多快?我是否需要切换到其他方法,例如使用像这样的连续分数?

log(x)

更新1:使用Wikipedia中提到的双曲线arctan系列,计算似乎比C标准库日志函数慢了近2.2倍。虽然,我还没有广泛检查性能,但是对于更大的数量,我当前的实现似乎真的很慢。如果可以管理的话,我想检查我的实现的错误范围和平均时间,以获取各种数字。这是我的第二项努力。

double series_ln_arctanh(double n){ /* n = e^a * b, where a is an non-negative integer */
    double lgVal = 0, term, now, sm;
    int i, flag = 1;

    if ( n <= 0 ) return 1e-300;
    if ( n * ee < 1 ) n = 1.0 / n, flag = -1; /* for extremely small n, use e^-x = 1/n */

    for ( term = 1; term < n ; term *= ee, lgVal++ );
    n /= term;

    /* log(x) = 2 arctanh((x-1)/(x+1)) */
    n = (1 - n)/(n + 1);

    now = term = n;
    n *= n;
    sm = 0;
    for ( i = 3 ; ; i += 2 ){
        sm += now;
        term *= n;
        now = term / i;
       if ( now < 1e-17 ) break;
    }

    lgVal -= 2*sm;

    if ( flag == -1 ) lgVal = -lgVal;
    return lgVal;
}

任何建议或批评表示赞赏。

1e81e3084e15

double series_ln_better(double n){ /* n = e^a * b, where a is an non-negative integer */
    double lgVal = 0, term, now, sm;
    int i, flag = 1;

    if ( n == 0 ) return -1./0.; /* -inf */
    if ( n < 0 ) return 0./0.;   /* NaN*/
    if ( n < 1 ) n = 1.0 / n, flag = -1; /* for extremely small n, use e^-x = 1/n */

    /* the cutoff iteration is 650, as over e**650, term multiplication would
       overflow. For larger numbers, the loop dominates the arctanh approximation
       loop (with having 13-15 iterations on average for tested numbers so far */

    for ( term = 1; term < n && lgVal < 650 ; term *= ee, lgVal++ );
    if ( lgVal == 650 ){
        n /= term;
        for ( term = 1 ; term < n ; term *= ee, lgVal++ );
    }
    n /= term;

    /* log(x) = 2 arctanh((x-1)/(x+1)) */
    n = (1 - n)/(n + 1);

    now = term = n;
    n *= n;
    sm = 0;

    /* limiting the iteration for worst case scenario, maximum 24 iteration */
    for ( i = 3 ; i < 50 ; i += 2 ){
        sm += now;
        term *= n;
        now = term / i;
        if ( now < 1e-17 ) break;
    }

    lgVal -= 2*sm;

    if ( flag == -1 ) lgVal = -lgVal;

    return lgVal;
}

Answers:


17

这并不是真正的权威性答案,更多的是我认为您应该考虑的问题列表,并且我尚未测试您的代码。

log2.15.1

f(x)doublen12

n1.7976e+308term=infn=11017nterm *= e709.78266108405500745

1030000

我怀疑您可以通过付出一些努力来牺牲一些鲁棒性来提高性能,例如,通过限制参数范围或返回精度稍差的结果。

3.这种代码的性能在很大程度上取决于它所运行的CPU体系结构。这是一个涉及很深的话题,但是像Intel这样的CPU制造商发布了优化指南,它们解释了您的代码与其运行的CPU之间的不同交互。缓存可能相对简单明了,但是在高级代码中很难准确地看到诸如分支预测,指令级并行性和由于数据依赖性导致的流水线停顿之类的事情,但对性能而言却至关重要。

x~y~=f~(x~)y=f(x~)准确?)。由于存在浮点舍入误差,这与表明泰勒级数收敛是不同的。

4.5。测试未经测试的函数准确性的一种好方法是,对四十亿(单精度浮点数)的四十亿个浮点数(如此处正确进行参数约简的次数减少)进行评估,然后将误差与​​来自libm。需要一些时间,但至少是彻底的。

5.因为从一开始就知道double的精度,所以您不必具有无限循环:可以预先计算出迭代次数(大约为50)。使用它可以从您的代码中删除分支,或者至少预先设置迭代次数。

关于循环展开的所有常规想法也适用。

6.可以使用泰勒级数以外的近似技术。也有Chebyshev系列(具有Clenshaw递归),Pade近似值,有时还可以找到根查找方法,例如Newton方法,只要您的函数可以重铸为更简单函数的根(例如,著名的sqrt技巧)。

连续的分数可能不会太大,因为它们涉及除法,这比乘/加要昂贵得多。如果你看一下_mm_div_sshttps://software.intel.com/sites/landingpage/IntrinsicsGuide/,师有延迟的吞吐量5-14 13-14个周期,并根据架构,以3-5 / 0.5-1比较用于乘法/加法/ madd。因此,总的来说(并非总是如此),尝试尽可能地消除划分是有意义的。

不幸的是,数学不是这样这里很大的指导,因为表情公式不一定是最快的。例如,数学不会惩罚除法。

x=m×2em12<m1exfrexp

8.比较你logloglibmopenlibm(:如https://github.com/JuliaLang/openlibm/blob/master/src/e_log.c)。到目前为止,这是找出其他人已经知道的最简单的方法。也有针对libm CPU制造商的经过特别优化的版本,但通常不会发布其源代码。

Boost :: sf具有一些特殊功能,但没有基本功能。不过,查看log1p的源可能具有指导意义:http : //www.boost.org/doc/libs/1_58_0/libs/math/doc/html/math_toolkit/powers/log1p.html

还有诸如mpfr之类的开源任意精度算术库,由于需要更高的精度,因此它们可能使用与libm不同的算法。

9. Higham的数值算法的准确性和稳定性是分析数值算法错误的一个很好的上层介绍。对于逼近算法本身,Trefethen的逼近理论逼近实践是一个很好的参考。

10.我知道这种说法经常被提及,但是相当大的软件项目很少依赖一次又一次调用一个小函数的运行时。不必担心日志的性能不是常见,除非您已对程序进行了概要分析并确保它很重要。


26414e15

1.13e13term

 1e8

1
k=11071lnk

2
frexp x=m×2elnx=eln2+lnm

5

基里尔的答案已经涉及到许多相关问题。我想根据实际的数学库设计经验来扩展其中的一些内容。预先注意:数学库设计人员倾向于使用每个已发布的算法优化以及许多特定于机器的优化,但并不是所有的优化都会被发布。该代码经常以汇编语言编写,而不是使用编译后的代码。因此,假设具有相同的功能集(准确性,特殊情况处理,错误报告,舍入模式支持),那么简单且经过编译的实现不可能达到现有高质量数学库实现的75%以上的性能。

explogerfcΓ

通常通过与(第三方)高精度参考进行比较来评估准确性。单参数单精度函数可以轻松轻松地进行详尽的测试,而其他函数则需要使用(定向)随机测试向量进行测试。显然,不能计算出无限精确的参考结果,但对制表机难题的研究表明,对于许多简单功能,足以以大约目标精度三倍的精度计算参考。参见例如:

VincentLefèvre,Jean-Michel Muller,“双精度的基本函数正确舍入的最坏情况”。在会议论文集第15届IEEE计算机算术研讨会上,2001,111-118。(在线预印本)

在性能方面,必须区分优化延迟(在查看相关操作的执行时间时很重要)与优化吞吐量(在考虑独立操作的执行时间时相关)之间。在过去的20年中,诸如指令级并行性(例如,超标量,乱序处理器),数据级并行性(例如,SIMD指令)和线程级并行性(例如,超线程,多核处理器)已导致将计算吞吐量作为更相关的指标。

log(1+x)=p(x)log(x)=2atanh((x1)/(x+1))=p(((x1)/(x+1))2)p

融合乘法加法运算(FMA)于25年前由IBM首次提出,现在可在所有主要处理器体系结构上使用,是现代数学库实现的重要组成部分。它提供了舍入误差减小的功能,为减法消除提供了有限的保护,并极大地简化了双精度算法

C99log()C99fma()233

#include <math.h>

/* compute natural logarithm

   USE_ATANH == 1: maximum error found: 0.83482 ulp @ 0.7012829191167614
   USE_ATANH == 0: maximum error found: 0.83839 ulp @ 1.2788954397331760
*/
double my_log (double a)
{
    const double LOG2_HI = 0x1.62e42fefa39efp-01; // 6.9314718055994529e-01
    const double LOG2_LO = 0x1.abc9e3b39803fp-56; // 2.3190468138462996e-17
    double m, r, i, s, t, p, f, q;
    int e;

    m = frexp (a, &e);
    if (m < 0.70703125) { // 181/256
        m = m + m;
        e = e - 1;
    }
    i = (double)e;

    /* m in [181/256, 362/256] */

#if USE_ATANH
    /* Compute q = (m-1) / (m+1) */
    p = m + 1.0;
    m = m - 1.0;
    q = m / p;

    /* Compute (2*atanh(q)/q-2*q) as p(q**2), q in [-75/437, 53/309] */
    s = q * q;
    r =             0x1.2f1da230fb057p-3;  // 1.4800574027992994e-1
    r = fma (r, s,  0x1.399f73f934c01p-3); // 1.5313616375223663e-1
    r = fma (r, s,  0x1.7466542530accp-3); // 1.8183580149169243e-1
    r = fma (r, s,  0x1.c71c51a8bf129p-3); // 2.2222198291991305e-1
    r = fma (r, s,  0x1.249249425f140p-2); // 2.8571428744887228e-1
    r = fma (r, s,  0x1.999999997f6abp-2); // 3.9999999999404662e-1
    r = fma (r, s,  0x1.5555555555593p-1); // 6.6666666666667351e-1
    r = r * s;

    /* log(a) = 2*atanh(q) + i*log(2) = LOG2_LO*i + p(q**2)*q + 2q + LOG2_HI*i.
       Use K.C. Ng's trick to improve the accuracy of the computation, like so:
       p(q**2)*q + 2q = p(q**2)*q + q*t - t + m, where t = m**2/2.
    */
    t = m * m * 0.5;
    r = fma (q, t, fma (q, r, LOG2_LO * i)) - t + m;
    r = fma (LOG2_HI, i, r);

#else // USE_ATANH

    /* Compute f = m -1 */
    f = m - 1.0;
    s = f * f;

    /* Approximate log1p (f), f in [-75/256, 106/256] */
    r = fma (-0x1.961d64ddd82b6p-6, f, 0x1.d35fd598b1362p-5); // -2.4787281515616676e-2, 5.7052533321928292e-2
    t = fma (-0x1.fcf5138885121p-5, f, 0x1.b97114751d726p-5); // -6.2128580237329929e-2, 5.3886928516403906e-2
    r = fma (r, s, t);
    r = fma (r, f, -0x1.b5b505410388dp-5); // -5.3431043874398211e-2
    r = fma (r, f,  0x1.dd660c0bd22dap-5); //  5.8276198890387668e-2
    r = fma (r, f, -0x1.00bda5ecdad6fp-4); // -6.2680862565391612e-2
    r = fma (r, f,  0x1.1159b2e3bd0dap-4); //  6.6735934054864471e-2
    r = fma (r, f, -0x1.2489f14dd8883p-4); // -7.1420614809115476e-2
    r = fma (r, f,  0x1.3b0ee248a0ccfp-4); //  7.6918491287915489e-2
    r = fma (r, f, -0x1.55557d3b497c3p-4); // -8.3333481965921982e-2
    r = fma (r, f,  0x1.745d4666f7f48p-4); //  9.0909266480136641e-2
    r = fma (r, f, -0x1.999999d959743p-4); // -1.0000000092767629e-1
    r = fma (r, f,  0x1.c71c70bbce7c2p-4); //  1.1111110722131826e-1
    r = fma (r, f, -0x1.fffffffa61619p-4); // -1.2499999991822398e-1
    r = fma (r, f,  0x1.249249262c6cdp-3); //  1.4285714290377030e-1
    r = fma (r, f, -0x1.555555555f03cp-3); // -1.6666666666776730e-1
    r = fma (r, f,  0x1.999999999759ep-3); //  1.9999999999974433e-1
    r = fma (r, f, -0x1.fffffffffff53p-3); // -2.4999999999999520e-1
    r = fma (r, f,  0x1.555555555555dp-2); //  3.3333333333333376e-1
    r = fma (r, f, -0x1.0000000000000p-1); // -5.0000000000000000e-1

    /* log(a) = log1p (f) + i * log(2) */
    p = fma ( LOG2_HI, i, f);
    t = fma (-LOG2_HI, i, p);
    f = fma ( LOG2_LO, i, f - t);
    r = fma (r, s, f);
    r = r + p;
#endif // USE_ATANH

    /* Handle special cases */
    if (!((a > 0.0) && (a <= 0x1.fffffffffffffp1023))) {
        r = a + a;  // handle inputs of NaN, +Inf
        if (a  < 0.0) r =  0.0 / 0.0; //  NaN
        if (a == 0.0) r = -1.0 / 0.0; // -Inf
    }
    return r;
}

(+1)您是否知道通用的开源实现(如openlibm)是否足够好,还是可以改进其特殊功能?
基里尔

1
@Kirill最后,我研究了开源实现(很多年前),他们没有利用FMA的好处。当时,IBM Power和Intel Itanium是唯一包含此操作的体系结构,现在对其的硬件支持无处不在。同样,表加多项式近似是当时的最新技术,现在表不再受欢迎:内存访问导致更高的能源消耗,它们可能(并且确实)干扰矢量化,并且计算吞吐量的增长幅度超过了内存吞吐量导致表格可能对性能产生负面影响。
njuffa '16
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.