log(x)の高速近似について


10

少し前に、ライブラリ関数を使用せずにを計算しようとするコードを書きました。昨日、私は古いコードをレビューしていて、それを可能な限り速くするように試みました(そして正しい)。これまでの私の試みは次のとおりです。logバツ

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の対数値を追加します。aeaは1未満です。この時点で、log1xのテイラー展開は心配することなく使用できます。ealog1  バツ

最近、数値分析への関心が高まりました。そのため、このコードセグメントを実際にどれだけ速く実行できるか、十分に正確であるかという質問をしざるを得ません。このような継続分数を使用するなど、他のいくつかの方法に切り替える必要がありますか?

C標準ライブラリで提供される機能は、ほぼ5.1倍高速この実装よりなります。logバツ

更新1Wikipediaで言及されている双曲線arctanシリーズを使用すると、計算はC標準ライブラリのログ関数よりも約2.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;
}

提案や批判はありがたいです。

更新2:以下の提案に基づいて、標準のライブラリ実装よりも約2.5倍遅い、いくつかの増分変更をここに追加しました。しかし、私は整数のみのためにそれをテストしているランタイムが増加するであろう多数のため、この時間を、。今のところ。私はまだ、ランダムな二重の数字を生成する技術を知らない1 E 308をので、それはまだ完全にはベンチマークはありません。コードをより堅牢にするために、コーナーケースの修正を追加しました。私が行ったテストの平均誤差は約4 e 15です。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;
}

回答:


17

これは実際には信頼できる回答ではなく、検討すべき問題のリストであり、私はコードをテストしていません。

log2.15.1

fバツdouble12

1.7976e+308term=infn=11017term *= e709.78266108405500745

1030000

少し努力すれば、たとえば引数の範囲を制限したり、わずかに精度の低い結果を返したりするなど、パフォーマンスのロバスト性を犠牲にすることができると思います。

3.この種のコードのパフォーマンスは、実行されているCPUアーキテクチャに大きく依存します。これは深くて複雑なトピックですが、IntelのようなCPUメーカーは、コードとそれが実行されているCPUの間のさまざまな相互作用を説明する最適化ガイドを公開しています。キャッシュは比較的単純ですが、分岐予測、命令レベルの並列処理、データの依存関係によるパイプラインの停止などは、高レベルのコードでは正確に確認することは困難ですが、パフォーマンスにとっては重要です。

バツy=fバツy=fバツ正確?)。これは、浮動小数点丸め誤差の存在により、テイラー級数が収束することを示すのと同じではありません。

4.5。テストされていない関数の正確さをテストする良い方法は、40億(ここで、引数の削減を正しく行っている場合は少ない)単精度浮動小数点数のそれぞれで評価し、エラーを標準ログと比較することですlibm。少し時間がかかりますが、少なくともそれは完全です。

5.最初からdoubleの精度がわかっているので、無限のループを持っている必要はありません。反復数は前もって計算できます(おそらく約50)。これを使用してコードからブランチを削除するか、少なくとも事前に反復回数を設定します。

ループの展開に関する通常の考え方もすべて当てはまります。

6.テイラー級数以外の近似手法を使用することができます。また、チェビシェフシリーズ(クレンショウ回帰を使用)、パデ近似、および関数がより単純な関数のルートとして再キャストできるときはいつでも、ニュートン法などのルート探索メソッド(たとえば、有名なsqrtトリック)もあります。

継続的な分数は除算を伴うため、おそらくあまり大きくなりません。乗算/加算よりもはるかに高価です。あなたが見れば_mm_div_sshttps://software.intel.com/sites/landingpage/IntrinsicsGuide/、部門は3-5 / 0.5〜1と比較して、アーキテクチャに応じて、13〜14サイクルと5-14のスループットのレイテンシーを持っています乗算/加算/ madd。したがって、一般に(常にではありません)、可能な限り除算を排除することは理にかなっています。

残念ながら、数学ではありませんようにと表現するので、ここでは偉大なガイドの短い式が必ずしも最速のものではありません。たとえば、数学は除算にペナルティを課しません。

バツ=メートル×2eメートル12<メートル1eバツfrexp

8.あなたの比較logloglibmまたは openlibm(:例えば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.私はこれが少し頻繁に言われることを知っていますが、合理的に大きなソフトウェアプロジェクトは、何度も何度も呼び出される1つの小さな関数のランタイムに依存することはめったにありません。プログラムのプロファイルを作成し、それが重要であることを確認していない限り、ログのパフォーマンスについて心配する必要があることはそれほど一般的ではありません。


26414e15

1.13e13terメートル

整数でテストした場合、arctanh級数展開は平均13回の反復内に収束するようです。  1e8

1
Σk=11071lnk

2
frexp バツ=メートル×2elnバツ=eln2+lnメートル

5

キリルの答えはすでに多くの関連する問題に触れていました。実際の数学ライブラリの設計経験に基づいて、それらのいくつかを拡張したいと思います。事前に注意してください。数学ライブラリの設計者は、公開されているすべてのアルゴリズム最適化だけでなく、多くのマシン固有の最適化を使用する傾向があり、すべてが公開されるわけではありません。コードは、コンパイルされたコードを使用するのではなく、アセンブリ言語で頻繁に記述されます。したがって、同一の機能セット(精度、特殊なケースの処理、エラー報告、丸めモードのサポート)を前提として、単純でコンパイルされた実装が既存の高品質数学ライブラリ実装のパフォーマンスの75%以上を達成することはありません。

eバツplogerfcΓ

精度は通常、(サードパーティの)より精度の高いリファレンスとの比較によって評価されます。単引数単精度関数は簡単に徹底的にテストできますが、他の関数は(有向)ランダムテストベクトルでテストする必要があります。明らかに、無限に正確な参照結果を計算することはできませんが、Table-Makerのジレンマを調査すると、多くの単純な関数では、ターゲット精度の約3倍の精度で参照を計算すれば十分であることがわかります。例を見てください:

VincentLefèvre、Jean-Michel Muller、「倍精度での基本関数の正しい丸めの最悪のケース」。でコンピュータ算術の議事録第15回IEEEシンポジウム、2001,111-118)。(オンラインでプレプリント)

パフォーマンスの面では、レイテンシの最適化(依存する操作の実行時間を確認するときに重要)とスループットの最適化(独立した操作の実行時間を考慮する場合に関連)を区別する必要があります。過去20年間で、命令レベルの並列処理(スーパースカラー、順不同プロセッサなど)、データレベルの並列処理(SIMD命令など)、スレッドレベルの並列処理(ハイパースレッディングなど)などのハードウェア並列化技術の急増マルチコアプロセッサ)は、より関連性の高いメトリックとして、計算スループットに重点を置いています。

log1+バツ=pバツlogバツ=2atahバツ1/バツ+1=pバツ1/バツ+12p

25年前にIBMによって最初に導入され、現在すべての主要なプロセッサアーキテクチャで利用可能な融合型積和演算(FMA)は、現代の数学ライブラリ実装の重要なビルディングブロックです。これは、丸め誤差の低減を提供し、減算によるキャンセルに対して限定的な保護を提供し、double-double演算を大幅に簡素化します。

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など)が可能な限り優れているかどうか、またはそれらの特別な機能を改善できるかどうかを知っていますか?
Kirill、2016年

1
@Kirill最後に(何年も前に)オープンソースの実装を調べましたが、FMAの利点を活用していませんでした。当時、IBM PowerとIntel Itaniumがこの操作を含む唯一のアーキテクチャでしたが、現在では、そのハードウェアサポートがユビキタスになっています。また、テーブルと多項式の近似は最先端の技術でしたが、現在はテーブルが不利になっています。メモリアクセスによりエネルギー使用量が増加し、ベクトル化に干渉する可能性があり、実際に干渉し、計算スループットはメモリスループットよりも増加しています。その結果、テーブルからパフォーマンスに悪影響を及ぼす可能性があります。
njuffa
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.