О более быстром приближении log (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;
}

Здесь я пытаюсь найти так , что е чуть больше п, а затем добавить значение логарифм пaeanealog(1  x)

Недавно я заинтересовался численным анализом, и поэтому я не могу не задать вопрос, насколько быстрее этот сегмент кода может быть выполнен на практике, будучи достаточно правильным? Нужно ли мне переключаться на некоторые другие методы, например, используя продолжение дроби, как это ?

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

Любое предложение или критика приветствуется.

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. Можно использовать методы приближения, отличные от рядов Тейлора. Существуют также чебышевские ряды (с рекуррентностью Кленшоу), аппроксимации Паде и иногда методы поиска корней, такие как метод Ньютона, всякий раз, когда ваша функция может быть преобразована в корень более простой функции (например, известный трюк sqrt ).

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

К сожалению, математика не такая большая руководство здесь, потому что выражения с короткими формулами не обязательно являются самыми быстрыми. Математика не наказывает подразделения, например.

x=m×2em12<m1exfrexp

8. Сравните свои данные logс параметрами login 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. Точность и стабильность численных алгоритмов Higham - хорошее введение в анализ ошибок числовых алгоритмов. Для самих алгоритмов аппроксимации хорошим примером может служить практика аппроксимации теории приближений Трефетена.

10. Я знаю, что об этом говорят слишком часто, но достаточно крупные программные проекты редко зависят от времени выполнения одной маленькой функции, вызываемой снова и снова. Это не так распространено, чтобы беспокоиться о выполнении журнала, если вы не профилированные вашей программу и убедились , что это важно.


26414e15

1.13e13term

 1e8

1
k=11071lnk

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

5

Ответ Кирилла уже затронул большое количество актуальных вопросов. Я хотел бы остановиться на некоторых из них, основываясь на практическом опыте проектирования математических библиотек. Заранее: разработчики математических библиотек, как правило, используют каждую опубликованную алгоритмическую оптимизацию, а также множество машинно-специфических оптимизаций, не все из которых будут опубликованы. Код часто будет написан на ассемблере, а не скомпилированным кодом. Поэтому маловероятно, что простая и скомпилированная реализация достигнет более 75% производительности существующей высококачественной математической реализации библиотеки, предполагая идентичные наборы функций (точность, обработка особых случаев, отчеты об ошибках, поддержка режима округления).

explogerfcΓ

Точность обычно оценивается путем сравнения с (сторонним) эталоном с более высокой точностью. Функции с одинарной точностью и одним аргументом легко тестируются исчерпывающе, другие функции требуют тестирования с помощью (направленных) случайных тестовых векторов. Очевидно, что невозможно вычислить бесконечно точные эталонные результаты, но исследование дилеммы Table-Maker предполагает, что для многих простых функций достаточно вычислить эталон с точностью, примерно в три раза превышающей точность цели. Смотрите, например:

Винсент Лефевр, Жан-Мишель Мюллер, «Худшие случаи для правильного округления элементарных функций в двойной точности». В материалах 15-го симпозиума IEEE по компьютерной арифметике , 2001, 111-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 были единственными архитектурами, которые включали эту операцию, теперь ее аппаратная поддержка повсеместна. Кроме того, полиномиальные аппроксимации «таблица плюс» были в то время современным состоянием, теперь таблицы не в фаворе: доступ к памяти приводит к более высокому использованию энергии, они могут (и делают) мешать векторизации, а вычислительная пропускная способность увеличилась больше, чем пропускная способность памяти. в результате возможного негативного влияния на производительность из таблиц.
Нюффа
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.