2009-08-26 3 views
8

Я могу только предположить, что это ошибка. Первое утверждение проходит, пока второе не выполняется:gcc точность ошибка?

double sum_1 = 4.0 + 6.3; 
assert(sum_1 == 4.0 + 6.3); 

double t1 = 4.0, t2 = 6.3; 

double sum_2 = t1 + t2; 
assert(sum_2 == t1 + t2); 

Если не ошибка, то почему?

+0

Вы можете посмотреть также: - [http://stackoverflow.com/questions/21265/comparing-ieee-floats-and-doubles-for-equality](http://stackoverflow.com/questions/21265/ сравнение-ieee-floats-and-doubles-for-equal) - [http://stackoverflow.com/questions/17333/most-effective-way-for-float-and-double-comparison](http://stackoverflow .com/questions/17333/most-effective-way-for-float-and-double-сравнение) - [http://stackoverflow.com/questions/713763/strange-results-with-floating-point-comparison] (http://stackoverflow.com/questions/713763/strange-results-with-floating-point-comparison) – pingw33n

ответ

12

Это то, что меня укусило.

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

Но в этом случае вы вычисляете t1+t2, а затем вычисляете его снова. Несомненно,, который должен произвести идентичный результат?

Вот что, вероятно, происходит. Готов поспорить, что вы запускаете это на процессоре x86, правильно? FPU x86 использует 80 бит для своих внутренних регистров, но значения в памяти хранятся как 64-разрядные удваиваются.

Таким образом, t1+t2 сначала вычисляется с точностью до 80 бит, затем - предположим - сохраняется в памяти в sum_2 с 64-мя точками точности - и происходит некоторое округление. Для утверждения он загружается обратно в регистр с плавающей запятой, а t1+t2 вычисляется снова, снова с точностью до 80 бит. Итак, теперь вы сравниваете sum_2, который был ранее округлен до 64-битного значения с плавающей запятой, с t1+t2, который был рассчитан с более высокой точностью (80 бит) - и поэтому значения не совсем идентичны.

Редактировать Так почему же проходит первый тест? В этом случае компилятор, вероятно, оценивает 4.0+6.3 во время компиляции и сохраняет его как 64-битное количество - как для назначения, так и для assert.Таким образом сравниваются идентичные значения, и утверждение проходит.

Второй Редактировать Вот сборка код, сгенерированный для второй части кода (GCC, x86), с комментариями - в значительной степени следует вышеизложенному сценарию:

// t1 = 4.0 
fldl LC3 
fstpl -16(%ebp) 

// t2 = 6.3 
fldl LC4 
fstpl -24(%ebp) 

// sum_2 = t1+t2 
fldl -16(%ebp) 
faddl -24(%ebp) 
fstpl -32(%ebp) 

// Compute t1+t2 again 
fldl -16(%ebp) 
faddl -24(%ebp) 

// Load sum_2 from memory and compare 
fldl -32(%ebp) 
fxch %st(1) 
fucompp 

Интересное примечание: Это был скомпилирован без оптимизации. Когда он скомпилирован с -O3, компилятор оптимизирует все кода.

+0

Я не думаю, что это даже оно. Дело в том, что '4.0 + 6.3' - это выражение, которое компилятор получает с постоянной точностью 10.3. Таким образом, первое утверждение становится эквивалентным 'assert (10.3 == 10.3)', которое проходит тривиально. Во втором тесте на самом деле он занимает 4.0, помещает его в double, занимает 6.3, ставит его в два раза (теряя крошечный бит точности), добавляя их вместе и сравнивая * с * с константой 10.3, что терпит неудачу, потому что это разные на 2^-70 или около того. :) – hobbs

+0

Я не уверен ... если компилятор может оптимизировать t1 + t2 до 10.3 в инструкции assert, почему он не может сделать ту же оптимизацию в присваивании sum_2? –

+0

Глядя на листинг, вы меня достали. :) Безумно думаю, с моей стороны, я полагаю. – hobbs

13

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

void CompareFloats(double d1, double d2, double epsilon) 
{ 
    assert(abs(d1 - d2) < epsilon); 
} 

Это не имеет никакого отношения к компилятору, и все, что связано с тем, как реализованы числа с плавающей точкой. вот IEEE спецификация:

http://www.eecs.berkeley.edu/~wkahan/ieee754status/IEEE754.PDF

+4

То, что Ed вызывает прецизионную ошибку, является функцией арифметики с плавающей запятой. Нет никакого способа обойти это, это не ошибка, а не неосторожная реализация со стороны gcc-писателей, это то, как компьютеры делают арифметику на дроби. Google для «Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой» и передать свои уроки с сердцем. –

+0

Да, я добавил ссылку на спецификацию. –

+1

Google также имеет эту «ошибку»: http://www.google.com/search?hl=ru&source=hp&q=999999999999999+-+999999999999998&btnG=Google+Search&cts=1251328513752&aq=f&oq=&aqi= – llamaoo7

1

Сравнение чисел с двойной точностью по своей сути неточное. Например, вы можете найти 0.0 == 0.0 возвращение false. Это связано с тем, как FPU хранит и отслеживает номера.

Wikipedia says:

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

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

2

Возможно, в одном случае вы сравниваете 64-битный бит с 80-битным внутренним регистром. Это может быть полезно для ознакомления с инструкциями по сборке GCC для двух случаев ...

3

Я продублировал вашу проблему на своем Intel Core 2 Duo, и я посмотрел на код сборки. Вот что происходит: когда ваш компилятор оценивает t1 + t2, он делает

load t1 into an 80-bit register 
load t2 into an 80-bit register 
compute the 80-bit sum 

Когда он хранит в sum_2 он делает

round the 80-bit sum to a 64-bit number and store it 

Тогда == сравнение сравнивает 80-битную сумму 64-битную сумму а, и они различны, прежде всего потому, что дробная часть 0.3 не может быть точно представлена ​​с использованием двоичного числа с плавающей запятой, поэтому вы сравниваете «повторяющуюся десятичную» (фактически повторяющуюся двоичную), которая была усечена до двух разных длин.

Что действительно раздражает то, что если вы компилятором с gcc -O1 или gcc -O2, gcc выполняет неправильную арифметику во время компиляции, и проблема исчезает. Возможно, это нормально в соответствии со стандартом, но это еще одна причина, по которой gcc не является моим любимым компилятором.


P.S. Когда я говорю, что == сравнивает 80-битную сумму с 64-разрядной суммой, конечно, я действительно имею в виду, что она сравнивает расширенную версию 64-разрядной суммы. Вы можете сделать так, чтобы думать

sum_2 == t1 + t2 

решает

extend(sum_2) == extend(t1) + extend(t2) 

и

sum_2 = t1 + t2 

решает

sum_2 = round(extend(t1) + extend(t2)) 

Добро пожаловать в удивительный мир с плавающей точкой!

3

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

if (abs(x) != 0 || abs(y) != 0) 
    rel_diff (x, y) = abs((x - y)/max(abs(x),abs(y)) 
else 
    rel_diff(x,y) = max(abs(x),abs(y)) 

Например,

rel_diff(1.12345, 1.12367) = 0.000195787019 
rel_diff(112345.0, 112367.0) = 0.000195787019 
rel_diff(112345E100, 112367E100) = 0.000195787019 

Идея заключается в том, чтобы измерить число ведущих Значимые цифры имеют общее число; если вы берете -log10 из 0,000195787019, вы получаете 3.70821611, что составляет около 10 ведущих десятизначных базовых чисел, которые имеют общие примеры.

Если вам нужно определить, если два числа с плавающей точкой равны, вы должны сделать что-то вроде

if (rel_diff(x,y) < error_factor * machine_epsilon()) then 
    print "equal\n"; 

где машина эпсилон наименьшее число, которое может быть проведен в мантиссы точки аппаратных средств с плавающей используется. На большинстве компьютерных языков есть вызов функции для получения этого значения. error_factor должен основываться на количестве значительных цифр, которые, по вашему мнению, будут потребляться ошибками округления (и другими) при вычислениях чисел x и y. Например, если бы я знал, что x и y были результатом около 1000 суммирования и не знали каких-либо ограничений суммируемых чисел, я бы установил error_factor примерно в 100.

Пытался добавить их в качестве ссылок, но cann ' т, так как это мой первый пост:

  • en.wikipedia.org/wiki/Relative_difference
  • en.wikipedia.org/wiki/Machine_epsilon
  • en.wikipedia.org/wiki/Significand (мантисса)
  • ru.wikipedia.org/wiki/Rounding_error
0

Эта "проблема" может быть "фиксированной", используя следующие параметры:

-msse2 -mfpmath = ße

, как описано на этой странице:

http://www.network-theory.co.uk/docs/gccintro/gccintro_70.html

После ввода использовали эти опции, оба утверждения прошли.

Смежные вопросы