Вопрос: Сбита ли математика с плавающей запятой?


Рассмотрим следующий код:

0.1 + 0.2 == 0.3  ->  false
0.1 + 0.2         ->  0.30000000000000004

Почему происходят эти неточности?


2226


источник


Ответы:


двоичный плавающая точка математика такая. В большинстве языков программирования он основан на Стандарт IEEE 754 , JavaScript использует 64-битное представление с плавающей запятой, что совпадает с Java double, Суть проблемы состоит в том, что числа представлены в этом формате как целое число раз в два раза; рациональные числа (такие как 0.1, который 1/10), знаменатель которого не является степенью двух, не может быть точно представлен.

Для 0.1в стандартном binary64формат, представление можно записать точно так же, как

Напротив, рациональное число 0.1, который 1/10, можно записать точно так же, как

  • 0.1в десятичной системе или
  • 0x1.99999999999999...p-4в аналоге обозначений гексафлоата C99, где ...представляет бесконечную последовательность из 9.

Константы 0.2а также 0.3в вашей программе также будут приблизиться к их истинным значениям. Бывает, что самый близкий doubleв 0.2больше, чем рациональное число 0.2но что ближайший doubleв 0.3меньше рационального числа 0.3, Сумма 0.1а также 0.2превышает число рациональных чисел 0.3и, следовательно, не согласны с константой в вашем коде.

Достаточно комплексное рассмотрение арифметических вопросов с плавающей запятой Что каждый компьютерный ученый должен знать о арифметике с плавающей точкой , Для более простого объяснения см. floating-point-gui.de ,


1689



Перспектива конструктора оборудования

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

1. Обзор

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

2. Стандарты

Большинство процессоров следуют IEEE-754 но некоторые используют денормализованные или разные стандарты , Например, в IEEE-754 существует денормализованный режим, который позволяет отображать очень маленькие числа с плавающей запятой за счет точности. Однако следующее будет охватывать нормализованный режим IEEE-754, который является типичным режимом работы.

В стандарте IEEE-754 разработчикам аппаратного обеспечения допускается любое значение ошибки / эпсилон, если оно меньше половины одного устройства на последнем месте, и результат должен быть меньше половины одной единицы в последнем место для одной операции. Это объясняет, почему при повторных операциях ошибки складываются. Для двойной точности IEEE-754 это 54-й бит, поскольку 53 бита используются для представления числовой части (нормализованной), также называемой мантиссой, числа с плавающей запятой (например, 5.3 в 5.3e5). В следующих разделах более подробно рассматриваются причины аппаратной ошибки при различных операциях с плавающей запятой.

3. Причина ошибки округления в разделе

Основной причиной ошибки в делении с плавающей запятой являются алгоритмы деления, используемые для вычисления частного. Большинство компьютерных систем вычисляют деление с использованием умножения на обратное, в основном в Z=X/Y, Z = X * (1/Y), Разделение вычисляется итеративно, т. Е. Каждый цикл вычисляет некоторые биты частного, пока не будет достигнута желаемая точность, что для IEEE-754 - это что-либо с ошибкой менее одной единицы на последнем месте. Таблица обратных значений Y (1 / Y) известна как таблица выбора коэффициентов (QST) в медленном делении, а размер в битах таблицы выбора факторов обычно равен ширине основани или количеству битов фактор, вычисленный на каждой итерации, плюс несколько защитных бит. Для стандарта IEEE-754, двойной точности (64-бит), это будет размер радиуса делителя плюс несколько защитных бит k, где k>=2, Так, например, типичная таблица выбора коэффициентов для делителя, которая вычисляет 2 бита фактора за раз (radix 4), будет 2+2= 4бит (плюс несколько дополнительных бит).

3.1 Ошибка округления округления: аппроксимация взаимного

Какие обратные в таблице выбора факторов зависят от метод разделения : медленное деление, такое как разделение СТО, или быстрое деление, такое как дивизия Гольдшмидта; каждая запись модифицируется в соответствии с алгоритмом деления в попытке получить наименьшую возможную ошибку. В любом случае, все ответчики приближения фактического взаимного и ввести некоторый элемент ошибки. Оба метода медленного деления и быстрого деления вычисляют коэффициент итеративно, т. Е. Вычисляется каждый бит бита каждого шага, затем результат вычитается из дивиденда, а делитель повторяет этапы, пока ошибка не станет меньше половины одной единица на последнем месте. Методы медленного деления вычисляют фиксированное количество цифр частного на каждом шаге и обычно дешевле строить, а быстрые методы деления вычисляют переменное количество цифр на каждый шаг и обычно более дороги для сборки. Важнейшей частью методов разделения является то, что большинство из них полагаются на повторное умножение на приближение взаимности, поэтому они подвержены ошибкам.

4. Ошибки округления в других операциях: усечение

Другой причиной ошибок округления во всех операциях являются различные способы усечения окончательного ответа, который допускает IEEE-754. Есть усечение, округленное к нулю, от округлой до ближайшей (по умолчанию), округлением и округлением. Все методы вводят элемент ошибки менее одной единицы в последнем месте для одной операции. Со временем и повторяющимися операциями усечение также добавляет кумулятивно к полученной ошибке. Эта ошибка усечения особенно проблематична в возведении в степень, что связано с некоторой формой повторного умножения.

5. Повторные операции

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

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

6. Резюме

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


485



Когда вы конвертируете .1 или 1/10 в base 2 (двоичный), вы получаете повторяющийся шаблон после десятичной точки, точно так же, как пытаетесь представить 1/3 в базе 10. Значение не является точным, и поэтому вы не можете сделать точная математика с использованием обычных методов с плавающей запятой.


349



Most answers here address this question in very dry, technical terms. I'd like to address this in terms that normal human beings can understand.

Imagine that you are trying to slice up pizzas. You have a robotic pizza cutter that can cut pizza slices exactly in half. It can halve a whole pizza, or it can halve an existing slice, but in any case, the halving is always exact.

That pizza cutter has very fine movements, and if you start with a whole pizza, then halve that, and continue halving the smallest slice each time, you can do the halving 53 times before the slice is too small for even its high-precision abilities. At that point, you can no longer halve that very thin slice, but must either include or exclude it as is.

Now, how would you piece all the slices in such a way that would add up to one-tenth (0.1) or one-fifth (0.2) of a pizza? Really think about it, and try working it out. You can even try to use a real pizza, if you have a mythical precision pizza cutter at hand. :-)


Most experienced programmers, of course, know the real answer, which is that there is no way to piece together an exact tenth or fifth of the pizza using those slices, no matter how finely you slice them. You can do a pretty good approximation, and if you add up the approximation of 0.1 with the approximation of 0.2, you get a pretty good approximation of 0.3, but it's still just that, an approximation.

For double-precision numbers (which is the precision that allows you to halve your pizza 53 times), the numbers immediately less and greater than 0.1 are 0.09999999999999999167332731531132594682276248931884765625 and 0.1000000000000000055511151231257827021181583404541015625. The latter is quite a bit closer to 0.1 than the former, so a numeric parser will, given an input of 0.1, favour the latter.

(The difference between those two numbers is the "smallest slice" that we must decide to either include, which introduces an upward bias, or exclude, which introduces a downward bias. The technical term for that smallest slice is an ulp.)

In the case of 0.2, the numbers are all the same, just scaled up by a factor of 2. Again, we favour the value that's slightly higher than 0.2.

Notice that in both cases, the approximations for 0.1 and 0.2 have a slight upward bias. If we add enough of these biases in, they will push the number further and further away from what we want, and in fact, in the case of 0.1 + 0.2, the bias is high enough that the resulting number is no longer the closest number to 0.3.

In particular, 0.1 + 0.2 is really 0.1000000000000000055511151231257827021181583404541015625 + 0.200000000000000011102230246251565404236316680908203125 = 0.3000000000000000444089209850062616169452667236328125, whereas the number closest to 0.3 is actually 0.299999999999999988897769753748434595763683319091796875.


P.S. Some programming languages also provide pizza cutters that can split slices into exact tenths. Although such pizza cutters are uncommon, if you do have access to one, you should use it when it's important to be able to get exactly one-tenth or one-fifth of a slice.

(Originally posted on Quora.)


214



Floating point rounding errors. 0.1 cannot be represented as accurately in base-2 as in base-10 due to the missing prime factor of 5. Just as 1/3 takes an infinite number of digits to represent in decimal, but is "0.1" in base-3, 0.1 takes an infinite number of digits in base-2 where it does not in base-10. And computers don't have an infinite amount of memory.


198



In addition to the other correct answers, you may want to consider scaling your values to avoid problems with floating-point arithmetic.

For example:

var result = 1.0 + 2.0;     // result === 3.0 returns true

... instead of:

var result = 0.1 + 0.2;     // result === 0.3 returns false

The expression 0.1 + 0.2 === 0.3 returns false in JavaScript, but fortunately integer arithmetic in floating-point is exact, so decimal representation errors can be avoided by scaling.

As a practical example, to avoid floating-point problems where accuracy is paramount, it is recommended1 to handle money as an integer representing the number of cents: 2550 cents instead of 25.50 dollars.


1 Douglas Crockford: JavaScript: The Good Parts: Appendix A - Awful Parts (page 105).


97



My answer is quite long, so I've split it into three sections. Since the question is about floating point mathematics, I've put the emphasis on what the machine actually does. I've also made it specific to double (64 bit) precision, but the argument applies equally to any floating point arithmetic.

Preamble

An IEEE 754 double-precision binary floating-point format (binary64) number represents a number of the form

value = (-1)^s * (1.m51m50...m2m1m0)2 * 2e-1023

in 64 bits:

  • The first bit is the sign bit: 1 if the number is negative, 0 otherwise1.
  • The next 11 bits are the exponent, which is offset by 1023. In other words, after reading the exponent bits from a double-precision number, 1023 must be subtracted to obtain the power of two.
  • The remaining 52 bits are the significand (or mantissa). In the mantissa, an 'implied' 1. is always2 omitted since the most significant bit of any binary value is 1.

1 - IEEE 754 allows for the concept of a signed zero - +0 and -0 are treated differently: 1 / (+0) is positive infinity; 1 / (-0) is negative infinity. For zero values, the mantissa and exponent bits are all zero. Note: zero values (+0 and -0) are explicitly not classed as denormal2.

2 - This is not the case for denormal numbers, which have an offset exponent of zero (and an implied 0.). The range of denormal double precision numbers is dmin ≤ |x| ≤ dmax, where dmin (the smallest representable nonzero number) is 2-1023 - 51 (≈ 4.94 * 10-324) and dmax (the largest denormal number, for which the mantissa consists entirely of 1s) is 2-1023 + 1 - 2-1023 - 51 (≈ 2.225 * 10-308).


Turning a double precision number to binary

Many online converters exist to convert a double precision floating point number to binary (e.g. at binaryconvert.com), but here is some sample C# code to obtain the IEEE 754 representation for a double precision number (I separate the three parts with colons (:):

public static string BinaryRepresentation(double value)
{
    long valueInLongType = BitConverter.DoubleToInt64Bits(value);
    string bits = Convert.ToString(valueInLongType, 2);
    string leadingZeros = new string('0', 64 - bits.Length);
    string binaryRepresentation = leadingZeros + bits;

    string sign = binaryRepresentation[0].ToString();
    string exponent = binaryRepresentation.Substring(1, 11);
    string mantissa = binaryRepresentation.Substring(12);

    return string.Format("{0}:{1}:{2}", sign, exponent, mantissa);
}

Getting to the point: the original question

(Skip to the bottom for the TL;DR version)

Cato Johnston (the question asker) asked why 0.1 + 0.2 != 0.3.

Written in binary (with colons separating the three parts), the IEEE 754 representations of the values are:

0.1 => 0:01111111011:1001100110011001100110011001100110011001100110011010
0.2 => 0:01111111100:1001100110011001100110011001100110011001100110011010

Note that the mantissa is composed of recurring digits of 0011. This is key to why there is any error to the calculations - 0.1, 0.2 and 0.3 cannot be represented in binary precisely in a finite number of binary bits any more than 1/9, 1/3 or 1/7 can be represented precisely in decimal digits.

Converting the exponents to decimal, removing the offset, and re-adding the implied 1 (in square brackets), 0.1 and 0.2 are:

0.1 = 2^-4 * [1].1001100110011001100110011001100110011001100110011010
0.2 = 2^-3 * [1].1001100110011001100110011001100110011001100110011010

To add two numbers, the exponent needs to be the same, i.e.:

0.1 = 2^-3 *  0.1100110011001100110011001100110011001100110011001101(0)
0.2 = 2^-3 *  1.1001100110011001100110011001100110011001100110011010
sum = 2^-3 * 10.0110011001100110011001100110011001100110011001100111

Since the sum is not of the form 2n * 1.{bbb} we increase the exponent by one and shift the decimal (binary) point to get:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)

There are now 53 bits in the mantissa (the 53rd is in square brackets in the line above). The default rounding mode for IEEE 754 is 'Round to Nearest' - i.e. if a number x falls between two values a and b, the value where the least significant bit is zero is chosen.

a = 2^-2 * 1.0011001100110011001100110011001100110011001100110011
x = 2^-2 * 1.0011001100110011001100110011001100110011001100110011(1)
b = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

Note that a and b differ only in the last bit; ...0011 + 1 = ...0100. In this case, the value with the least significant bit of zero is b, so the sum is:

sum = 2^-2 * 1.0011001100110011001100110011001100110011001100110100

TL;DR

Writing 0.1 + 0.2 in a IEEE 754 binary representation (with colons separating the three parts) and comparing it to 0.3, this is (I've put the distinct bits in square brackets):

0.1 + 0.2 => 0:01111111101:0011001100110011001100110011001100110011001100110[100]
0.3       => 0:01111111101:0011001100110011001100110011001100110011001100110[011]

Converted back to decimal, these values are:

0.1 + 0.2 => 0.300000000000000044408920985006...
0.3       => 0.299999999999999988897769753748...

The difference is exactly 2-54, which is ~5.5511151231258 × 10-17 - insignificant (for many applications) when compared to the original values.

Comparing the last few bits of a floating point number is inherently dangerous, as anyone who reads the famous "What Every Computer Scientist Should Know About Floating-Point Arithmetic" (which covers all the major parts of this answer) will know.

Most calculators use additional guard digits to get around this problem, which is how 0.1 + 0.2 would give 0.3: the final few bits are rounded.


79



Floating point numbers stored in the computer consist of two parts, an integer and an exponent that the base is taken to and multiplied by the integer part.

If the computer were working in base 10, 0.1 would be 1 x 10⁻¹, 0.2 would be 2 x 10⁻¹, and 0.3 would be 3 x 10⁻¹. Integer math is easy and exact, so adding 0.1 + 0.2 will obviously result in 0.3.

Computers don't usually work in base 10, they work in base 2. You can still get exact results for some values, for example 0.5 is 1 x 2⁻¹ and 0.25 is 1 x 2⁻², and adding them results in 3 x 2⁻², or 0.75. Exactly.

The problem comes with numbers that can be represented exactly in base 10, but not in base 2. Those numbers need to be rounded to their closest equivalent. Assuming the very common IEEE 64-bit floating point format, the closest number to 0.1 is 3602879701896397 x 2⁻⁵⁵, and the closest number to 0.2 is 7205759403792794 x 2⁻⁵⁵; adding them together results in 10808639105689191 x 2⁻⁵⁵, or an exact decimal value of 0.3000000000000000444089209850062616169452667236328125. Floating point numbers are generally rounded for display.


47



Floating point rounding error. From What Every Computer Scientist Should Know About Floating-Point Arithmetic:

Squeezing infinitely many real numbers into a finite number of bits requires an approximate representation. Although there are infinitely many integers, in most programs the result of integer computations can be stored in 32 bits. In contrast, given any fixed number of bits, most calculations with real numbers will produce quantities that cannot be exactly represented using that many bits. Therefore the result of a floating-point calculation must often be rounded in order to fit back into its finite representation. This rounding error is the characteristic feature of floating-point computation.


40



My workaround:

function add(a, b, precision) {
    var x = Math.pow(10, precision || 2);
    return (Math.round(a * x) + Math.round(b * x)) / x;
}

precision refers to the number of digits you want to preserve after the decimal point during addition.


28



A lot of good answers have been posted, but I'd like to append one more.

Not all numbers can be represented via floats/doubles For example, the number "0.2" will be represented as "0.200000003" in single precision in IEEE754 float point standard.

Model for store real numbers under the hood represent float numbers as

enter image description here

Even though you can type 0.2 easily, FLT_RADIX and DBL_RADIX is 2; not 10 for a computer with FPU which uses "IEEE Standard for Binary Floating-Point Arithmetic (ISO/IEEE Std 754-1985)".

So it is a bit hard to represent such numbers exactly. Even if you specify this variable explicitly without any intermediate calculation.


26