Двойная равная 0 проблема в C

Я реализовал алгоритм вычисления натуральных логов в C.

double taylor_ln(int z) {
    double sum = 0.0;
    double tmp = 1.0;

    int i = 1;
    while(tmp != 0.0) {
        tmp = (1.0 / i) * (pow(((z - 1.0) / (z + 1.0)), i));
        printf("(1.0 / %d) * (pow(((%d - 1.0) / (%d + 1.0)), %d)) = %f\n", i, z, z, i, tmp);
        sum += tmp;
        i += 2;
    }

    return sum * 2;
}

Как показано в заявлении на печать, tmp делает равным 0.0 в конце концов, однако цикл продолжается. Что может быть причиной этого?

Я на Fedora 14 amd64 и компилирую с помощью:

clang -lm -o taylor_ln taylor_ln.c

Пример:

$ ./taylor_ln 2
(1.0 / 1) * (pow(((2 - 1.0) / (2 + 1.0)), 1)) = 0.333333
(1.0 / 3) * (pow(((2 - 1.0) / (2 + 1.0)), 3)) = 0.012346
(1.0 / 5) * (pow(((2 - 1.0) / (2 + 1.0)), 5)) = 0.000823
(1.0 / 7) * (pow(((2 - 1.0) / (2 + 1.0)), 7)) = 0.000065
(1.0 / 9) * (pow(((2 - 1.0) / (2 + 1.0)), 9)) = 0.000006
(1.0 / 11) * (pow(((2 - 1.0) / (2 + 1.0)), 11)) = 0.000001
(1.0 / 13) * (pow(((2 - 1.0) / (2 + 1.0)), 13)) = 0.000000
(1.0 / 15) * (pow(((2 - 1.0) / (2 + 1.0)), 15)) = 0.000000
(1.0 / 17) * (pow(((2 - 1.0) / (2 + 1.0)), 17)) = 0.000000
(1.0 / 19) * (pow(((2 - 1.0) / (2 + 1.0)), 19)) = 0.000000
(1.0 / 21) * (pow(((2 - 1.0) / (2 + 1.0)), 21)) = 0.000000
and so on...

Ответ 1

Сравнение с плавающей точкой является точным, поэтому 10^-10 не совпадает с 0.0.

В принципе, вы должны сравнивать с некоторой допустимой разницей, например 10^-7 на основе количества десятичных знаков, которые вы пишете, которые можно выполнить как:

while(fabs(tmp) > 10e-7)

Ответ 2

Не используйте точные операции равенства при работе с числами с плавающей запятой. Хотя ваш номер может выглядеть как 0, он может быть чем-то вроде 0.00000000000000000000001.

Вы увидите это, если вместо %f использовать %.50f в строках формата. Последний использует разумное значение по умолчанию для десятичных знаков (6 в вашем случае), но первый явно указывает, что вы хотите много.

Для обеспечения безопасности используйте дельту, чтобы проверить, достаточно ли она закрыта, например:

if (fabs (val) < 0.0001) {
    // close enough.
}

Очевидно, что дельта полностью зависит от ваших потребностей. Если вы говорите деньги, 10 -5 может быть много. Если вы физик, вы, вероятно, должны выбрать меньшее значение.

Конечно, если вы математик, неточность достаточно мала: -)

Ответ 3

Просто потому, что число, отображаемое как "0.000000", не означает, что оно равно 0.0. Десятичное отображение чисел имеет меньшую точность, чем двойное хранилище.

Возможно, ваш алгоритм приближается к точке, где он очень близок к 0, но следующий шаг движется так мало, что он округляется до того же самого, что и раньше, и, следовательно, он никогда не приближается к 0 ( просто переходит в бесконечный цикл).

В общем случае вам не следует сравнивать числа с плавающей запятой с == и !=. Вы всегда должны проверять, находятся ли они в пределах небольшого диапазона (обычно называемого epsilon). Например:

while(fabs(tmp) >= 0.0001)

Затем он остановится, когда он будет достаточно близко к 0.

Ответ 4

Оператор печати отображает округленное значение, оно не печатает максимально возможную точность. Таким образом, ваша петля еще не достигла нуля.

(И, как говорили другие, из-за проблем округления он никогда не сможет достичь этого. Поэтому сравнение значения с небольшим пределом является более надежным, чем сравнение для равенства с 0.0.)

Ответ 5

Много обсуждений причины, но здесь альтернативное решение:

double taylor_ln(int z)
{
    double sum = 0.0;
    double tmp, old_sum;
    int i = 1;
    do 
    {
        old_sum = sum;
        tmp = (1.0 / i) * (pow(((z - 1.0) / (z + 1.0)), i));
        printf("(1.0 / %d) * (pow(((%d - 1.0) / (%d + 1.0)), %d)) = %f\n",
               i, z, z, i, tmp);
        sum += tmp;
        i += 2;
    } while (sum != old_sum);
    return sum * 2;
 }

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

Обратите внимание, что когда вы суммируете относительно большое число с относительно небольшим, значимые цифры результата ограничивают точность. В противоположность этому, если вы суммируете несколько небольших, то добавьте это к большому, у вас может быть достаточно, чтобы немного поднять большой. В вашем алгоритме небольшие значения tmp не суммировались друг с другом в любом случае, поэтому нет накопления, если каждый из них фактически не влияет на сумму - следовательно, описанный выше подход работает без дополнительной компрометации точности.