Tentang perkiraan log yang lebih cepat (x)

10

Saya telah menulis sebuah kode waktu lalu yang berusaha untuk menghitung tanpa menggunakan fungsi perpustakaan. Kemarin, saya meninjau kode lama, dan saya mencoba membuatnya secepat mungkin, (dan benar). Inilah upaya saya sejauh ini:lHaig(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;
}

Di sini saya mencoba untuk menemukan sehingga e a hanya lebih n, dan kemudian saya menambahkan nilai logaritma dari nSebuaheSebuah , yang kurang dari 1. Pada titik ini, ekspansi Taylor darilog(1-x)dapat digunakan tanpa mengkhawatirkan.neSebuahlHaig(1 - x)

Saya baru-baru ini semakin tertarik pada analisis numerik, dan itulah sebabnya saya tidak bisa tidak bertanya, seberapa cepat segmen kode ini dapat dijalankan dalam praktik, sementara cukup benar? Apakah saya perlu beralih ke beberapa metode lain, misalnya, menggunakan fraksi lanjutan, seperti ini ?

The fungsi yang disediakan dengan C standar perpustakaan hampir 5,1 kali lebih cepat dari implementasi ini.lHaig(x)

UPDATE 1 : Menggunakan seri arctan hiperbolik yang disebutkan di Wikipedia , perhitungannya tampaknya hampir 2,2 kali lebih lambat daripada fungsi log library C standar. Padahal, saya belum memeriksa kinerja secara ekstensif, dan untuk jumlah yang lebih besar, implementasi saya saat ini tampaknya SANGAT lambat. Saya ingin memeriksa kedua implementasi saya untuk batas kesalahan dan waktu rata-rata untuk berbagai nomor jika saya dapat mengaturnya. Ini adalah usaha kedua saya.

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

Setiap saran atau kritik dihargai.

UPDATE 2: Berdasarkan saran yang dibuat di bawah ini, saya telah menambahkan beberapa perubahan tambahan di sini, yang sekitar 2,5 kali lebih lambat dari implementasi perpustakaan standar. Namun, saya telah mengujinya hanya untuk bilangan bulat kali ini, untuk jumlah yang lebih besar runtime akan meningkat. Untuk sekarang. Saya belum tahu teknik untuk menghasilkan angka ganda acak 1 e 308 , jadi belum sepenuhnya menjadi patokan. Untuk membuat kode lebih kuat, saya telah menambahkan koreksi untuk kasus sudut. Kesalahan rata-rata untuk tes yang saya buat adalah sekitar 4 e - 15 .1e81e3084e-15

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;
}
sarker306
sumber

Jawaban:

17

Ini tidak benar-benar berwibawa jawaban, lebih daftar masalah yang saya pikir Anda harus mempertimbangkan, dan saya belum diuji kode Anda.

log2.15.1

1. Masalah umum dengan mengevaluasi suatu fungsif(x)doublen12

1.5. Sudahkah Anda memeriksa kode Anda untuk besar ? Saya mencoba , yang mengarah ke , dan kemudian ke dalam loop seri Taylor, yang mengarah ke konvergensi yang sangat lambat: ia menyatu seperti seri Harmonic, yaitu tidak tetapi akan ada paling banyak 10 17 istilah. Sebagai aturan praktis, Anda harus memiliki semacam "jumlah maksimum iterasi" terikat untuk loop. Dalam hal ini berperilaku seperti ini karenan1.7976e+308term=infn=11017nterm *= e709.78266108405500745...

10-30000-

Saya menduga bahwa dengan sedikit usaha Anda dapat mengorbankan sebagian dari kekuatan itu untuk kinerja, misalnya, dengan membatasi rentang argumen atau mengembalikan hasil yang sedikit kurang akurat.

3. Kinerja kode semacam ini dapat sangat bergantung pada arsitektur CPU yang digunakan. Ini adalah topik yang mendalam dan terlibat, tetapi produsen CPU seperti Intel menerbitkan panduan pengoptimalan yang menjelaskan berbagai interaksi antara kode Anda dan CPU yang digunakan. Caching bisa relatif mudah, tetapi hal-hal seperti prediksi cabang, paralelisme tingkat instruksi, dan warung pipa karena ketergantungan data sulit dilihat secara tepat dalam kode tingkat tinggi, tetapi sangat penting untuk kinerja.

x~y~=f~(x~)y=f(x~) bagaimanatepat?). Ini tidak sama dengan menunjukkan bahwa seri Taylor bertemu, karena adanya kesalahan pengakhiran titik-mengambang.

4.5. Cara yang baik untuk menguji fungsi yang tidak teruji untuk akurasi, adalah dengan mengevaluasinya di masing-masing dari empat miliar (lebih sedikit jika Anda melakukan pengurangan argumen dengan benar, seperti di sini) mengapung presisi tunggal, dan membandingkan kesalahan dengan log standar dari libm. Butuh sedikit waktu, tetapi setidaknya itu menyeluruh.

5. Karena Anda tahu dari awal ketepatan ganda, Anda tidak harus memiliki loop tanpa batas: jumlah iterasi dapat ditemukan di depan (mungkin sekitar 50). Gunakan ini untuk menghapus cabang dari kode Anda, atau setidaknya setel jumlah iterasi sebelumnya.

Semua ide yang biasa tentang loop membuka gulungan juga berlaku.

6. Dimungkinkan untuk menggunakan teknik perkiraan selain dari seri Taylor. Ada juga seri Chebyshev (dengan pengulangan Clenshaw), pendekatan Pade, dan kadang-kadang metode pencarian root seperti metode Newton setiap kali fungsi Anda dapat disusun kembali sebagai akar dari fungsi yang lebih sederhana (misalnya, trik sqrt yang terkenal ).

Fraksi lanjutan mungkin tidak akan terlalu besar, karena melibatkan pembagian, yang jauh lebih mahal daripada mengalikan / menambahkan. Jika Anda melihat _mm_div_ssdi https://software.intel.com/sites/landingpage/IntrinsicsGuide/ , divisi memiliki latency 13-14 siklus dan throughput 5-14, tergantung pada arsitektur, dibandingkan dengan 3-5 / 0.5-1 untuk multiply / add / madd. Jadi secara umum (tidak selalu) masuk akal untuk mencoba menghilangkan divisi sebanyak mungkin.

Sayangnya, matematika tidak seperti panduan yang besar di sini, karena ekspresi dengan singkat formula belum tentu yang tercepat. Matematika tidak menghukum divisi, misalnya.

x=m×2em12<m1ex jauh lebih alami daripada log base-2, yang bagian pertama dari kode Anda dapat diganti dengan satu panggilan frexp.

8. Bandingkan Anda logdengan login libmatau openlibm(misalnya: https://github.com/JuliaLang/openlibm/blob/master/src/e_log.c ). Sejauh ini, ini adalah cara termudah untuk mengetahui apa yang sudah diketahui orang lain. Ada juga versi khusus yang dioptimalkan libm untuk produsen CPU, tetapi biasanya kode sumbernya tidak dipublikasikan.

Boost :: sf memiliki beberapa fungsi khusus, tetapi bukan yang dasar. Mungkin instruktif untuk melihat sumber log1p, meskipun: http://www.boost.org/doc/libs/1_58_0/libs/math/doc/html/math_toolkit/powers/log1p.html

Ada juga pustaka aritmatika presisi-terbuka sumber-terbuka seperti mpfr, yang mungkin menggunakan algoritma yang berbeda dari libm karena dibutuhkan ketelitian yang lebih tinggi.

9. Akurasi dan Stabilitas Algoritma Numerik Higham adalah pengantar tingkat atas yang baik untuk menganalisis kesalahan algoritma numerik. Untuk algoritme aproksimasi sendiri, Praktik Teori Aproksimasi oleh Trefethen adalah referensi yang bagus.

10. Saya tahu ini dikatakan terlalu sering, tetapi proyek perangkat lunak yang cukup besar jarang bergantung pada runtime dari satu fungsi kecil yang dipanggil berulang kali. Ini tidak begitu umum harus khawatir tentang kinerja log, kecuali Anda telah diprofilkan program anda dan memastikan itu penting.

Kirill
sumber
264-14e-15
1.13e-13term
 1e8
1
k=1107-1dalamk
2
@ sarker306 Anda dapat menghilangkan loop pertama seluruhnya dengan menggunakan frexpuntuk mendapatkan mantissa dan eksponen nomor tersebut x=m×2edalamx=edalam2+dalamm
5

Jawaban Kirill sudah menyentuh sejumlah besar masalah yang relevan. Saya ingin memperluas beberapa dari mereka berdasarkan pengalaman desain perpustakaan matematika praktis. Catatan di muka: perancang perpustakaan matematika cenderung menggunakan setiap optimasi algoritme yang dipublikasikan, serta banyak optimisasi spesifik mesin, tidak semuanya akan dipublikasikan. Kode sering akan ditulis dalam bahasa assembly, daripada menggunakan kode yang dikompilasi. Oleh karena itu tidak mungkin bahwa implementasi langsung dan dikompilasi akan mencapai lebih dari 75% dari kinerja implementasi perpustakaan matematika berkualitas tinggi yang ada, dengan asumsi set fitur yang identik (akurasi, penanganan kasus khusus, pelaporan kesalahan, dukungan mode pembulatan).

exhallHaigerfcΓ

Akurasi biasanya dinilai dengan perbandingan dengan referensi presisi tinggi (pihak ketiga). Argumen tunggal Fungsi presisi tunggal dapat dengan mudah diuji secara mendalam, fungsi lainnya memerlukan pengujian dengan vektor uji acak (diarahkan). Jelas seseorang tidak dapat menghitung hasil referensi yang sangat akurat, tetapi penelitian terhadap Dilema Table-Maker's menunjukkan bahwa untuk banyak fungsi sederhana, cukup untuk menghitung referensi dengan ketepatan sekitar tiga kali ketepatan target. Lihat misalnya:

Vincent Lefèvre, Jean-Michel Muller, "Kasus Terburuk untuk Pembulatan Benar dari Fungsi Dasar dalam Presisi Ganda". Dalam Prosiding Simposium IEEE 15 tentang Aritmatika Komputer , 2001,111-118). (pracetak online)

Dalam hal kinerja, kita harus membedakan antara mengoptimalkan latensi (penting ketika kita melihat waktu pelaksanaan operasi tergantung), versus mengoptimalkan untuk throughput (relevan ketika mempertimbangkan waktu pelaksanaan operasi independen). Selama dua puluh tahun terakhir, proliferasi teknik paralelisasi perangkat keras seperti paralelisme tingkat instruksi (misalnya superscalar, prosesor out-of-order), paralelisme tingkat data (misalnya instruksi SIMD), dan paralelisme tingkat-benang (misalnya hyper-threading, prosesor multi-core) telah mengarah pada penekanan pada throughput komputasi sebagai metrik yang lebih relevan.

lHaig(1+x)=hal(x)lHaig(x)=2SebuahtSebuahnh((x-1)/(x+1))=hal(((x-1)/(x+1))2)hal

Operasi penggandaan multiply-add ( FMA ), pertama kali diperkenalkan oleh IBM 25 tahun yang lalu, dan sekarang tersedia di semua arsitektur prosesor utama, adalah blok bangunan penting dari implementasi perpustakaan matematika modern. Ini memberikan pengurangan kesalahan pembulatan, memberikan perlindungan terbatas terhadap pembatalan subtraktif , dan sangat menyederhanakan aritmatika ganda-ganda .

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;
}
njuffa
sumber
(+1) Apakah Anda tahu apakah implementasi open-source umum (seperti openlibm) sebaik mungkin, atau dapatkah fungsi-fungsi khusus mereka ditingkatkan?
Kirill
1
@ Kirill Terakhir saya melihat implementasi open source (bertahun-tahun yang lalu), mereka tidak mengeksploitasi manfaat FMA. Pada saat itu IBM Power dan Intel Itanium adalah satu-satunya arsitektur yang termasuk operasi, sekarang dukungan perangkat keras untuk itu ada di mana-mana. Selain itu, perkiraan tabel-plus-polinomial adalah yang paling canggih saat itu, sekarang tabel tidak disukai: akses memori menghasilkan penggunaan energi yang lebih tinggi, mereka dapat (dan memang) mengganggu vektorisasi, dan throughput komputasi telah meningkat lebih dari throughput memori menghasilkan dampak kinerja potensial negatif dari tabel.
njuffa