web-dev-qa-db-ja.com

C ++の非常に高速な近似対数(自然対数)関数?

_std::sqrt_( タイミング平方根 )と_std::exp_( より高速な指数近似の使用 )を置き換えるさまざまなトリックがありますが、 _std::log_を置き換えます。

これは私のプログラムのループの一部であり、複数回呼び出されます。expとsqrtが最適化されている間、Intel VTuneは_std::log_を最適化するように提案します。その後、私の設計上の選択のみが制限されるようです。

今のところ、ln(1+x)の3次テイラー近似をxで_-0.5_と_+0.5_の間で使用します(最大誤差が4%の場合の90%)。それ以外の場合は、_std::log_にフォールバックします。これにより、15%のスピードアップが得られました。

8
user3091460

パフォーマンスのための超越関数のカスタマイズされた実装の設計と展開に着手する前に、ツールチェーンだけでなく、アルゴリズムレベルでの最適化を追求することを強くお勧めします。残念ながら、ここで最適化するコードに関する情報も、ツールチェーンに関する情報もありません。

アルゴリズムレベルで、超越関数へのすべての呼び出しが本当に必要かどうかを確認します。多分、より少ない関数呼び出しを必要とする、または超越関数を代数演算に変換する数学的変換があります。超越関数呼び出しのいずれかが冗長である可能性がありますか?計算が対数空間に不必要に切り替わっているので?精度要件が中程度の場合、全体でfloatの代わりにdoubleを使用して、計算全体を単精度で実行できますか?ほとんどのハードウェアプラットフォームでは、doubleの計算を回避すると、パフォーマンスが大幅に向上する可能性があります。

コンパイラは、数値を多用するコードのパフォーマンスに影響を与えるさまざまなスイッチを提供する傾向があります。一般的な最適化レベルを_-O3_に上げることに加えて、非正規化サポートをオフにする方法、つまりゼロへのフラッシュ、つまりFTZモードをオンにする方法がよくあります。これには、さまざまなハードウェアプラットフォームでパフォーマンス上の利点があります。さらに、「高速計算」フラグがよくあります。このフラグを使用すると、精度がわずかに低下し、NaNや無限大などの特殊なケースを処理するためのオーバーヘッドがなくなり、さらにerrnoが処理されます。一部のコンパイラは、コードの自動ベクトル化もサポートしており、IntelコンパイラなどのSIMD数学ライブラリが付属しています。

対数関数のカスタム実装では、通常、バイナリ浮動小数点引数xを指数eと仮数mに分離し、_x = m * 2_のようにします。eしたがって、log(x) = log(2) * e + log(m)mは、1に近くなるように選択されます。これは、効率的な近似を提供するためです。たとえば、log(m) = log(1+f) = log1p(f) by ミニマックス多項式近似

C++は、浮動小数点オペランドを仮数と指数に分離するfrexp()関数を提供しますが、実際には、通常、浮動小数点データを次のように再解釈してビットレベルで操作するより高速なマシン固有のメソッドを使用します。同じサイズの整数。以下の単精度対数のコードlogf()は、両方のバリアントを示しています。関数__int_as_float()および__float_as_int()は、_int32_t_をIEEE-754 _binary32_浮動小数点数に、またはその逆に再解釈することを提供します。このコードは、現在のほとんどのプロセッサ、CPU、またはGPUのハードウェアで直接サポートされている融合積和演算FMAに大きく依存しています。 fmaf()がソフトウェアエミュレーションにマップされるプラットフォームでは、このコードは許容できないほど遅くなります。

_#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;
}
_

コードコメントに記載されているように、上記の実装は忠実に丸められた単精度の結果を提供し、IEEE-754浮動小数点標準と一致する例外的なケースを処理します。特殊なケースのサポートを排除し、非正規化引数のサポートを排除し、精度を下げることで、パフォーマンスをさらに向上させることができます。これにより、次のバリアントが例示されます。

_/* 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;
}
_
9
njuffa

this の議論を見てください。受け入れられた答えは、ゼッケンドルフ分解に基づいて対数を計算するための関数の 実装 を参照しています。

実装ファイルのコメントには、O(1)に到達するための複雑さといくつかのトリックについての議論があります。

お役に立てれば!

3
Mo Abdul-Hameed
#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
}
1
Saar Ibuki