С++ Ошибка числового усечения

извините если туплю, но не нашел ответа.

#include <iostream>

using namespace std;

int main()
{
double a(0);
double b(0.001);
cout << a - 0.0 << endl;
for (;a<1.0;a+=b);
cout << a - 1.0 << endl;
for (;a<10.0;a+=b);
cout << a - 10.0 << endl;
cout << a - 10.0-b << endl;
return 0;
}

Вывод:
0
6,66134e-16
0,001
-1,03583e-13

Пробовал компилировать с помощью MSVC9, MSVC10, Borland C++ 2010. Все они приходят в итоге к ошибке примерно 1e-13. Нормально ли иметь такое значительное накопление ошибок всего за 1000, 10000 приращений?


person Andrew    schedule 07.05.2010    source источник
comment
docs.sun.com/source/806-3568/ncg_goldberg.html   -  person Anycorn    schedule 07.05.2010
comment
home.comcast.net/~ tom_forsyth/ (не я, хех)   -  person dash-tom-bang    schedule 07.05.2010


Ответы (4)


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

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

person WhirlWind    schedule 07.05.2010
comment
Спасибо, я понимаю ошибку усечения и все остальное дерьмо. Проделал много теоретической работы по численным методам, но я просто никогда не проверял это сам и был очень удивлен, узнав, насколько он велик... - person Andrew; 07.05.2010
comment
Что ж, двойное число дает примерно 16 знаков после запятой, это правильно. Когда вы выполняете цикл 1000 раз, ваша точность снижается примерно до 13 цифр. - person WhirlWind; 07.05.2010
comment
Имейте в виду, что это особенно болезненно только с иррациональными (по основанию 2) числами. Если вы используете степени двойки, все будет в порядке. Например. если ваш сумматор был 0,5, 0,25, 0,125, 0,0625 и т. д., они в конечном счете составят точно 1,0, так как эти значения в базе 2 равны 0,1, 0,01, 0,001 и 0,0001. - person dash-tom-bang; 07.05.2010
comment
@dash-tom-bang Хороший вопрос, хотя я бы сказал, что если вы занимаетесь такими вещами, возможно, вам лучше вписаться в целочисленную арифметику. - person WhirlWind; 07.05.2010
comment
@ WhirlWind: где-то в глубине души я ожидал, что она будет накапливаться медленнее, чем линейно, из-за случайности характера ошибки, но теперь я понимаю, что это была просто фантазия :) - person Andrew; 07.05.2010
comment
Ну, если у вас есть фиксированное количество знаков после запятой, целые числа всегда будут выигрывать. Например. не вычисляйте доллары США с плавающей запятой, представляйте их целыми числами, где 1 - это один цент или, может быть, какая-то доля цента. - person dash-tom-bang; 07.05.2010

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

if( foo == 0.0 ){
    //code here
}

и вместо этого сделать

bool checkFloat(float _input, float _compare, float _epsilon){
    return ( _input + _epsilon > _compare ) && ( _input - _epsilon < _compare );
}
person wheaties    schedule 07.05.2010
comment
Проверка на ноль удобна во многих случаях, поскольку она может быть точно представлена ​​в аппаратном обеспечении, но, конечно, это зависит от того, что вы с ней делаете... Начиная с 1 и вычитая 0,1 десять раз, вы не получите 0, т.е. курс. - person dash-tom-bang; 07.05.2010
comment
Это очень верно, но в большинстве случаев это просто не очень хорошая практика. Я люблю ошибаться на стороне осторожности. - person wheaties; 07.05.2010
comment
ошибаюсь из-за осторожности... забавно ;) Или, возможно, ошибаюсь из-за размера эпсилон. - person WhirlWind; 07.05.2010

думать об этом. каждая операция вносит небольшую ошибку, но следующая операция использует немного ошибочный результат. учитывая достаточное количество итераций, вы отклонитесь от истинного результата. если хотите, напишите свои выражения в форме t0 = (t + y + e), t1 = (t0 + y +e) и вычислите термины с эпсилон. по их условиям можно оценить приблизительную погрешность.

есть и второй источник ошибки: в какой-то момент вы комбинируете относительно малые и относительно большие числа ближе к концу. если вы помните определение машинной точности, 1 + e = 1, в какой-то момент операции будут терять значимые биты.

Надеюсь, это поможет прояснить с точки зрения непрофессионалов

person Anycorn    schedule 07.05.2010

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

При сравнении двух чисел вы не можете просто сказать a == b, потому что одно может быть 0, а другое -1.03583e-13 из-за потери точности в операциях с плавающей запятой, применяемых для получения a и b. Вы должны выбрать произвольный допуск, например: fabs(a,b) < 1e-8.

При печати числа часто требуется ограничить количество печатаемых цифр. Если вы используете printf, вы можете сказать printf("%g\n", a);, что не будет печатать такие вещи, как -1.03583e-13. Я не знаю, есть ли iostream аналог %g; здесь?

person Joey Adams    schedule 07.05.2010