Неожиданный результат использования POSIX ceil () в Perl - PullRequest
2 голосов
/ 04 марта 2010

Я не могу понять, почему следующее дает такой результат.

use POSIX;
my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;
my $c = ceil($n);
print "$c ($n)\n";

Сигил-тастик, я знаю - извините.

Я решил это для своего приложения следующим образом:

use POSIX;
my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;
my $c = ceil("$n");
print "$c ($n)\n";

... но я изумлен, почему это необходимо здесь.

Ответы [ 6 ]

8 голосов
/ 04 марта 2010

Происходит следующее: $n содержит значение с плавающей запятой и, следовательно, не совсем равно 3, на моем компьютере это 3.00000000000000044409. Perl достаточно умен, чтобы округлить его до 3 при печати, но когда вы явно используете функцию с плавающей запятой, он будет делать именно то, что объявляет: ceil это до следующего целого числа: 4.

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

6 голосов
/ 04 марта 2010
use strict;
use warnings;
use POSIX;

my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;  # Should be exactly 3.

# But it's not.
print "Equals 3\n" if $n == 3;

# Check it more closely.
printf "%.18f\n", $n;

# So ceil() is doing the right thing after all.
my $c = ceil($n);
print "g=$g t=$t r=$r n=$n c=$c\n";
5 голосов
/ 04 марта 2010

Обязательная ссылка Голдберга: Что должен знать каждый компьютерщик об арифметике с плавающей точкой .

Используя Perl, возможность обрабатывать строку как число в числовой операции превращается в преимущество, потому что вы можете легко использовать sprintf, чтобы явно указать желаемую степень точности:

use strict; use warnings;

use POSIX qw( ceil );
my $g = 6.65;
my $t = $g * 4;
my $r = $t - $g;
my $n = $r / $g;
my $c = ceil( sprintf '%.6f', $n );
print "$c ($n)\n";

Выход:

C:\Temp> g
3 (3)

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

4 голосов
/ 04 марта 2010

Некоторые числа (например, 6.65) имеют точное представление в десятичном виде, но не могут быть точно представлены в двоичной с плавающей запятой, которую используют компьютеры (точно так же, как 1/3 не имеет точного десятичного представления). В результате числа с плавающей запятой часто немного отличаются от ожидаемых. Результат вашего расчета не 3, а около 3.000000000000000444.

Традиционный способ справиться с этим - определить небольшое число (называемое эпсилон), а затем считать два числа равными, если они отличаются меньше, чем эпсилон.

Ваше решение ceil("$n") работает, потому что Perl округляет число с плавающей запятой примерно до 14 десятичных знаков при преобразовании его в строку (таким образом, преобразовывая 3.000000000000000444 обратно в 3). Но более быстрое решение состоит в том, чтобы вычесть эпсилон (поскольку округляется ceil) до вычисления ceil:

my $epsilon = 5e-15; # Or whatever small number you feel is appropriate
my $c = ceil($n - $epsilon);

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

4 голосов
/ 04 марта 2010

Не проблема Perl, как таковая

#include <stdlib.h>
#include <math.h>
#include <stdio.h>
main()
{
  double n = (6.65 * 4.0 - 6.65) / 6.65;
  double c = ceil(n);
  printf("c is %g, n was %.18f\n", c, n);
}

c is 4, n was 3.000000000000000444
1 голос
/ 05 марта 2010

Другие ответы объяснили, почему проблема существует, есть два способа ее устранения.

Если вы можете, вы можете скомпилировать Perl для использования типов с более высокой точностью. Конфигурирование с -Duse64bitint -Duselongdouble заставит Perl использовать 64-битные целые и длинные двойные числа. Они имеют достаточно высокую точность, чтобы устранить большинство проблем с плавающей запятой.

Другой альтернативой является использование bignum , которое включает прозрачную поддержку числа произвольной точности. Это медленнее, но точнее и может использоваться лексически.

{
    use bignum;
    use POSIX;
    my $g = 6.65;
    my $t = $g * 4;
    my $r = $t - $g;
    my $n = $r / $g;
    my $c = ceil($n);
    print "$c ($n)\n";
}

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

...