Про швидше наближення журналу (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;
}

Тут я намагаюся знайти так , що ті трохи більше п, а потім додати значення логарифм пaea , що менше 1. У цей момент розширення Тейлораlog(1-x)можна використовувати, не переживаючи.nealog(1  x)

Нещодавно у мене з'явився інтерес до чисельного аналізу, і тому я не можу не задати питання, наскільки швидше цей сегмент коду можна запустити на практиці, дотримуючись при цьому достатньо правильності? Чи потрібно переходити до якихось інших методів, наприклад, використовуючи продовження дробу, як це ?

Функція надається зі стандартною бібліотекою С, майже у 5,1 рази швидша, ніж ця реалізація.log(x)

ОНОВЛЕННЯ 1 : Використовуючи згаданий у Вікіпедії гіперболічний арктановий ряд , здається, що обчислення майже в 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 е - 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(x)doublen12

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

1030000

Я підозрюю, що, доклавши трохи зусиль, ви можете пожертвувати частиною цієї надійності для виконання, наприклад, обмеживши діапазон аргументів або повернувши трохи менш точні результати.

3. Продуктивність такого типу коду може багато залежати від архітектури процесора, на якій він працює. Це глибока та зайнята тема, але виробники процесорів, як Intel, публікують посібники з оптимізації, які пояснюють різні взаємодії між вашим кодом та процесором, на якому він працює. Кешування може бути досить простим, але такі речі, як прогнозування гілок, паралелізм на рівні інструкцій та зупинки конвеєра через залежність від даних, важко побачити саме в коді високого рівня, але важливі для роботи.

x~y~=f~(x~)y=f(x~)точний?). Це не те саме, що показувати, що ряд Тейлора сходиться через наявність помилок округлення з плаваючою комою.

4.5. Хорошим способом перевірити неперевірену функцію на точність, було б оцінити її на кожному з чотирьох мільярдів (менше, якщо ви робите зменшення аргументів правильно, як тут), одноточна плаває, і порівняти помилки зі стандартним журналом з libm. Займає небагато часу, але принаймні ретельно.

5. Оскільки ви з самого початку знаєте точність подвоєнь, вам не доведеться мати без обмеженого циклу: кількість ітерацій можна визначити вперед (це, мабуть, близько 50). Використовуйте це, щоб видалити гілки зі свого коду або принаймні встановити кількість повторень заздалегідь.

Усі звичні ідеї про розкручування циклу також застосовуються.

6. Можна використовувати методи наближення, відмінні від серії Тейлора. Існують також серії Чебішева (з повторенням Кленшова), апроксимації Pade, а іноді й методи кореневого пошуку, як метод Ньютона, коли ваша функція може бути перероблена як корінь більш простої функції (наприклад, знаменитий трюк sqrt ).

Продовження дробів, ймовірно, не буде занадто великим, оскільки вони передбачають поділ, що набагато дорожче, ніж множення / додавання. Якщо ви подивіться на _mm_div_ssна https://software.intel.com/sites/landingpage/IntrinsicsGuide/ , розподіл має латентність 13-14 циклів і пропускну здатність 5-14, в залежності від архітектури, по порівнянні з 3-5 / 0,5-1 для множення / додавання / мадд. Тому в цілому (не завжди) є сенс намагатися максимально усунути поділи.

На жаль, математика тут не такий чудовий посібник, оскільки вирази з короткими формулами не обов'язково є найшвидшими. Наприклад, математика не карає поділів.

x=m×2em12<m1exfrexp

8. Порівняйте свою інформацію logз logвхідним libmабо openlibm(наприклад: https://github.com/JuliaLang/openlibm/blob/master/src/e_log.c ). Це, безумовно, найпростіший спосіб з'ясувати, що вже зрозуміли інші люди. Існують також спеціально оптимізовані версії, libm характерні для виробників процесорів, але зазвичай не публікується їх вихідний код.

Boost :: sf має деякі особливі функції, але не основні. Однак, може бути корисним переглянути джерело log1p: http://www.boost.org/doc/libs/1_58_0/libs/math/doc/html/math_toolkit/powers/log1p.html

Існують також бібліотеки з арифметикою з довільною точністю з відкритим кодом, такі як mpfr, які можуть використовувати різні алгоритми, ніж libm через більш високу необхідну точність.

9. Точність і стабільність чисельних алгоритмів Хігхема - це хороше введення на верхньому рівні до аналізу помилок чисельних алгоритмів. Для самих алгоритмів наближення хороша орієнтація є практикою наближення теорії наближення від Трефетена.

10. Я знаю, що про це говорять занадто часто, але досить великі програмні проекти рідко залежать від часу виконання однієї невеликої функції знову і знову. Побоюватися про роботу журналу не так часто, якщо ви не профілювали програму і не переконалися, що це важливо.


26414e15

1.13e13term

 1e8

1
k=11071lnk

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

5

Відповідь Кирила вже торкнулася великої кількості відповідних питань. Я хотів би розширити деякі з них на основі практичного досвіду з математичної бібліотеки. Попередня примітка: дизайнери математичних бібліотек, як правило, використовують кожну опубліковану алгоритмічну оптимізацію, а також безліч специфічних оптимізацій машин, не всі з яких будуть опубліковані. Код часто записується мовою складання, а не використовувати складений код. Тому малоймовірно, що пряма та складена реалізація дозволить досягти понад 75% ефективності існуючої високоякісної математичної реалізації бібліотеки, передбачаючи однакові набори функцій (точність, обробка окремих випадків, повідомлення про помилки, підтримка режиму округлення).

explogerfcΓ

Точність, як правило, оцінюється порівнянням із посиланням на більш високу точність (стороннім). Функції з одноточною одноточністю легко перевірити вичерпно, інші функції вимагають тестування за допомогою (спрямованих) випадкових векторів тесту. Очевидно, що неможливо обчислити нескінченно точні еталонні результати, але дослідження дилеми виробника таблиць дозволяють припустити, що для багатьох простих функцій достатньо обчислити посилання з точністю, приблизно втричі більшою від цільової точності. Див. Наприклад:

Вінсент Лефевр, Жан-Мішель Мюллер, "Найгірші випадки правильного округлення елементарних функцій у подвійній точності". У працях 15-го симпозіуму ІЕЕЕ з комп'ютерної арифметики , 2001, 1111-118). (передрук в Інтернеті)

З точки зору продуктивності слід розрізняти оптимізацію затримки (важливо, коли дивиться час виконання залежних операцій), порівняно з оптимізацією для пропускної здатності (актуально при розгляді часу виконання незалежних операцій). Протягом останніх двадцяти років розповсюдження технічних засобів паралелізації апаратних засобів, таких як паралелізм рівня інструкцій (наприклад, суперскаляр, процесори поза замовленням), паралелізм рівня даних (наприклад, інструкції SIMD) та паралелізм на рівні потоку (наприклад, гіпер-нитка, багатоядерні процесори) привели до акценту на обчислювальній пропускній здатності як на більш відповідну метрику.

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

З'єднана операція множення-додавання ( FMA ), вперше представлена ​​IBM 25 років тому, і тепер доступна для всіх основних архітектурних процесорів, є важливим складовим елементом сучасних реалізацій математичної бібліотеки. Він забезпечує зменшення помилок округлення, забезпечує обмежений захист від віднімання віднімання та значно спрощує подвійну-подвійну арифметику .

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.