代码之家  ›  专栏  ›  技术社区  ›  qwark

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

  •  18
  • qwark  · 技术社区  · 8 年前

    std::sqrt ( Timing Square Root )还有一些是为了 std::exp ( Using Faster Exponential Approximation ),但我找不到任何替代品 std::log .

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

    现在我使用三阶泰勒近似 ln(1+x) 具有 x 之间 -0.5 +0.5 (90%的情况下,最大误差为4%),然后回落到 标准::日志

    6 回复  |  直到 6 年前
        1
  •  16
  •   njuffa    4 年前

    在开始设计和部署超越功能的定制实现以提高性能之前,最好在算法级别以及通过工具链进行优化。不幸的是,我们没有关于这里要优化的代码的任何信息,也没有关于工具链的信息。

    在算法级别,检查是否所有对超越函数的调用都是真正必要的。也许有一种数学转换需要更少的函数调用,或者将超越函数转换为代数运算。是否有任何超越函数调用可能是多余的,例如,因为计算不必要地切换对数空间?如果精度要求适中,可以使用 float 而不是 double 自始至终在大多数硬件平台上,避免 双重的 计算可以显著提高性能。

    编译器倾向于提供各种开关,这些开关会影响数字密集型代码的性能。除了将一般优化级别提高到 -O3 errno

    对数函数的自定义实现通常涉及分隔二进制浮点参数 x 变成指数 e m ,因此 x = m * 2 e 因此 log(x) = log(2) * e + log(m) . 被选择为接近统一,因为这提供了有效的近似,例如 log(m) = log(1+f) = log1p(f) 通过 minimax polynomial approximation .

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

    #include <cmath>
    #include <cstdint>
    #include <cstring>
    
    float __int_as_float (int32_t a) { float r; memcpy (&r, &a, sizeof r); return r;}
    int32_t __float_as_int (float a) { int32_t r; memcpy (&r, &a, sizeof r); return r;}
    
    /* compute natural logarithm, maximum error 0.85089 ulps */
    float my_logf (float a)
    {
        float i, m, r, s, t;
        int e;
    
    #if PORTABLE
        m = frexpf (a, &e);
        if (m < 0.666666667f) {
            m = m + m;
            e = e - 1;
        }
        i = (float)e;
    #else // PORTABLE
        i = 0.0f;
        if (a < 1.175494351e-38f){ // 0x1.0p-126
            a = a * 8388608.0f; // 0x1.0p+23
            i = -23.0f;
        }
        e = (__float_as_int (a) - __float_as_int (0.666666667f)) & 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] */
        m = m - 1.0f;
        s = m * m;
        /* Compute log1p(m) for m in [-1/3, 1/3] */
        r =             -0.130310059f;  // -0x1.0ae000p-3
        t =              0.140869141f;  //  0x1.208000p-3
        r = fmaf (r, s, -0.121483512f); // -0x1.f198b2p-4
        t = fmaf (t, s,  0.139814854f); //  0x1.1e5740p-3
        r = fmaf (r, s, -0.166846126f); // -0x1.55b36cp-3
        t = fmaf (t, s,  0.200120345f); //  0x1.99d8b2p-3
        r = fmaf (r, s, -0.249996200f); // -0x1.fffe02p-3
        r = fmaf (t, m, r);
        r = fmaf (r, m,  0.333331972f); //  0x1.5554fap-2
        r = fmaf (r, m, -0.500000000f); // -0x1.000000p-1  
        r = fmaf (r, s, m);
        r = fmaf (i,  0.693147182f, r); //  0x1.62e430p-1 // log(2)
        if (!((a > 0.0f) && (a < INFINITY))) {
            r = a + a;  // silence NaNs if necessary
            if (a  < 0.0f) r = INFINITY - INFINITY; //  NaN
            if (a == 0.0f) r = -INFINITY;
        }
        return r;
    }
    

    /* 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;
    }
    
        2
  •  4
  •   Community CDub    7 年前

    看看吧 this 讨论,接受的答案是指 implementation 基于Zeckendorf分解计算对数的函数。

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

    希望这有帮助!

        3
  •  2
  •   Saar Ibuki    6 年前
    #include <math.h>
    #include <iostream>
    
    constexpr int LogPrecisionLevel = 14;
    constexpr int LogTableSize = 1 << LogPrecisionLevel;
    
    double log_table[LogTableSize];
    
    void init_log_table() {
        for (int i = 0; i < LogTableSize; i++) {
            log_table[i] = log2(1 + (double)i / LogTableSize);
        }
    }
    
    double fast_log2(double x) { // x>0
        long long t = *(long long*)&x;
        int exp = (t >> 52) - 0x3ff;
        int mantissa = (t >> (52 - LogPrecisionLevel)) & (LogTableSize - 1);
        return exp + log_table[mantissa];
    }
    
    int main() {
        init_log_table();
    
        double d1 = log2(100); //6.6438561897747244
        double d2 = fast_log2(100); //6.6438561897747244
        double d3 = log2(0.01); //-6.6438561897747244
        double d4 = fast_log2(0.01); //-6.6438919626096089
    }
    
        4
  •  0
  •   Kari    4 年前

    我矢量化了@njuffa的答案。自然日志,与AVX2配合使用:

    inline __m256 mm256_fmaf(__m256 a, __m256 b, __m256 c){
        return _mm256_add_ps(_mm256_mul_ps(a, b), c);
    }
    
    //https://stackoverflow.com/a/39822314/9007125
    //https://stackoverflow.com/a/65537754/9007125
    // vectorized version of the answer by njuffa
    /* natural log on [0x1.f7a5ecp-127, 0x1.fffffep127]. Maximum relative error 9.4529e-5 */
    inline __m256 fast_log_sse(__m256 a){
    
        __m256i aInt = *(__m256i*)(&a);
        __m256i e =    _mm256_sub_epi32( aInt,  _mm256_set1_epi32(0x3f2aaaab));
                e =    _mm256_and_si256( e,  _mm256_set1_epi32(0xff800000) );
            
        __m256i subtr =  _mm256_sub_epi32(aInt, e);
        __m256 m =  *(__m256*)&subtr;
    
        __m256 i =  _mm256_mul_ps( _mm256_cvtepi32_ps(e), _mm256_set1_ps(1.19209290e-7f));// 0x1.0p-23
        /* m in [2/3, 4/3] */
        __m256 f =  _mm256_sub_ps( m,  _mm256_set1_ps(1.0f) );
        __m256 s =  _mm256_mul_ps(f, f); 
        /* Compute log1p(f) for f in [-1/3, 1/3] */
        __m256 r =  mm256_fmaf( _mm256_set1_ps(0.230836749f),  f,  _mm256_set1_ps(-0.279208571f) );// 0x1.d8c0f0p-3, -0x1.1de8dap-2
        __m256 t =  mm256_fmaf( _mm256_set1_ps(0.331826031f),  f,  _mm256_set1_ps(-0.498910338f) );// 0x1.53ca34p-2, -0x1.fee25ap-2
    
               r =  mm256_fmaf(r, s, t);
               r =  mm256_fmaf(r, s, f);
               r =  mm256_fmaf(i, _mm256_set1_ps(0.693147182f),  r);  // 0x1.62e430p-1 // log(2)
        return r;
    }
    
        5
  •  0
  •   Kevin Meier    2 年前

    我还需要一个快速的对数近似值,到目前为止最好的似乎是基于Ankerls算法的近似值。

    http://martin.ankerl.com/2007/10/04/optimized-pow-approximation-for-java-and-c-c/

    代码(复制自 https://github.com/ekmett/approximate/blob/dc1ee7cef58a6b31661edde6ef4a532d6fc41b18/cbits/fast.c#L127 ):

    double log_fast_ankerl(double a)
    {
      static_assert(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__, "Little endian is required!");
      union { double d; int x[2]; } u = { a };
      return (u.x[1] - 1072632447) * 6.610368362777016e-7;
    }
    

    只有一个减法和乘法。它出乎意料地好,而且速度快得无与伦比。

        6
  •  -2
  •   Malcolm McLean    8 年前

    这取决于你需要有多准确。通常调用log来了解数字的大小,通过检查浮点数的指数字段,基本上可以免费执行此操作。这也是你的第一近似值。我将为我的书《基本算法》插入一个插件,该书解释了如何从第一原理实现标准库数学函数。