php сложение чисел с плавающей точкой

Точность чисел с плавающей точкой

Числа с плавающей точкой имеют ограниченную точность. Хотя это зависит от операционной системы, в PHP обычно используется формат двойной точности IEEE 754, дающий максимальную относительную ошибку округления порядка 1.11e-16. Неэлементарные арифметические операции могут давать большие ошибки, и, разумеется, необходимо принимать во внимание распространение ошибок при совместном использовании нескольких операций.

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

«Простое» объяснение можно найти в » руководстве по числам с плавающей точкой, которое также называется «Why don’t my numbers add up?» («Почему мои числа не складываются?»)

Преобразование в число с плавающей точкой

Из строк

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

Из других типов

Для значений других типов преобразование выполняется путём преобразования значения сначала в целое число ( int ), а затем в число с плавающей точкой ( float ). Смотрите Преобразование в целое число для получения дополнительной информации.

Поскольку определённые типы имеют неопределённое поведение при преобразовании в целое число ( int ), то же самое происходит и при преобразовании в число с плавающей точкой ( float ).

Сравнение чисел с плавающей точкой

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

Для сравнения чисел с плавающей точкой используется верхняя граница относительной ошибки при округлении. Эта величина называется машинной эпсилон или единицей округления (unit roundoff) и представляет собой самую маленькую допустимую разницу при расчётах.

= 1.23456789 ;
$b = 1.23456780 ;
$epsilon = 0.00001 ;

User Contributed Notes 36 notes

PHP thinks that 1.6 (coming from a difference) is not equal to 1.6. To make it work, use round()

var_dump(round($x, 2) == round($y, 2)); // this is true

While the author probably knows what they are talking about, this loss of precision has nothing to do with decimal notation, it has to do with representation as a floating-point binary in a finite register, such as while 0.8 terminates in decimal, it is the repeating 0.110011001100. in binary, which is truncated. 0.1 and 0.7 are also non-terminating in binary, so they are also truncated, and the sum of these truncated numbers does not add up to the truncated binary representation of 0.8 (which is why (floor)(0.8*10) yields a different, more intuitive, result). However, since 2 is a factor of 10, any number that terminates in binary also terminates in decimal.

I’d like to point out a «feature» of PHP’s floating point support that isn’t made clear anywhere here, and was driving me insane.

Will fail in some cases due to hidden precision (standard C problem, that PHP docs make no mention of, so I assumed they had gotten rid of it). I should point out that I originally thought this was an issue with the floats being stored as strings, so I forced them to be floats and they still didn’t get evaluated properly (probably 2 different problems there).

To fix, I had to do this horrible kludge (the equivelant of anyway):

if (round($a,3)>=round($b,3)) echo «blah!»;

THIS works. Obviously even though var_dump says the variables are identical, and they SHOULD BE identical (started at 0.01 and added 0.001 repeatedly), they’re not. There’s some hidden precision there that was making me tear my hair out. Perhaps this should be added to the documentation?

Concider the following:

19.6*100 cannot be compaired to anything without manually
casting it as something else first.

Rule of thumb, if it has a decimal point, use the BCMath functions.

The ‘floating point precision’ box in practice means:

This returns 0.1 and is the workaround we use.

So, that’s all lovely then.

In some cases you may want to get the maximum value for a float without getting «INF».

var_dump(1.8e308); will usually show: float(INF)

I wrote a tiny function that will iterate in order to find the biggest non-infinite float value. It comes with a configurable multiplicator and affine values so you can share more CPU to get a more accurate estimate.

I haven’t seen better values with more affine, but well, the possibility is here so if you really thing it’s worth the cpu time, just try to affine more.

Best results seems to be with mul=2/affine=1. You can play with the values and see what you get. The good thing is this method will work on any system.

Beware of NaN and strings in PHP.
In other languages (and specifically in Javascript) math operations with non-numerical strings will result in NaN, while in PHP the string is silently converted to 0.

is_nan(‘hello, string’); // false

gives the impression that the string is a valid number.

Be careful when using float values in strings that are used as code later, for example when generating JavaScript code or SQL statements. The float is actually formatted according to the browser’s locale setting, which means that «0.23» will result in «0,23». Imagine something like this:

This would result in a different result for users with some locales. On most systems, this would print:

but when for example a user from Germany arrives, it would be different:

which is obviously a different call to the function. JavaScript won’t state an error, additional arguments are discarded without notice, but the function doBar(a) would get 0 as parameter. Similar problems could arise anywhere else (SQL, any string used as code somewhere else). The problem persists, if you use the «.» operator instead of evaluating the variable in the string.

So if you REALLY need to be sure to have the string correctly formatted, use number_format() to do it!

To simply convert 32 bits float from hex to float:

To compare two numbers use:

In the gettype() manual, it says «(for historical reasons «double» is returned in case of a float, and not simply «float») «.

However, I think that internally PHP sometimes uses the C double definition (i.e. a double is twice the size of a float/real). See the example below:

(The strrev_x-bin2hex combination is just to give printable characters.)

Given that PHP treats doubles and floats identically, I’d expected the same string as output, however, the output is:

double pack
string(16) «3ff999999999999a» //Here you see that there is a minute difference.
string(16) «3ff9999999999998»
float pack
string(8) «3fcccccd» //. which doesn’t exist here
string(8) «3fcccccd»

Convert a hex string into a 32-bit IEEE 754 float number. This function is 2 times faster then the below hex to 32bit function. This function only changes datatypes (string to int) once. Also, this function is a port from the hex to 64bit function from below.

But, please don’t use your own «functions» to «convert» from float to binary and vice versa. Looping performance in PHP is horrible. Using pack/unpack you use processor’s encoding, which is always correct. In C++ you can access the same 32/64 data as either float/double or 32/64 bit integer. No «conversions».

PHP switches from the standard decimal notation to exponential notation for certain «special» floats. You can see a partial list of such «special» values with this:

I have to be honest: this is one of the strangest things I have seen in any language in over 20 years of coding, and it is a colossal pain to work around.

Just another note about the locales. Consider the following code:

convert 32bit HEX values into IEEE 754 floating point
= «C45F82ED» ;

I’ve just come across this issue with floats when writing a function for pricing. When converting from string to a float, with 2 digits of precision, the issue with comparing floats can pop up and give inconsistent results due to the conversion process.

An easier way rather than relying on the mentioned epsilon method is to use number_format (at least for me as I’ll remember it!).

Example function that can return an unexpected result:

if((float)$a == (float)$b) <
echo true;
> else <
echo false;
>

echo’s false in this example.

Using number format here to trim down the precision (2 point precision being mostly used for currencies etc, although higher precisions should be correctly catered for by number_format), will return an expected result:

if(number_format((float)$a, 2) == number_format((float)$b, 2)) <
echo true;
> else <
echo false;
>

Correctly echo’s true.

My BIN to FLOAT (IEEE754), the first one doesn’t work for me:

As «m dot lebkowski+php at gmail dot com» (http://www.php.net/language.types.float#81416) noted 9 comments below :

When PHP converts a float to a string, the decimal separator used depends on the current locale conventions.

Calculations involving float types become inaccurate when it deals with numbers with more than approximately 8 digits long where ever the decimal point is. This is because of how 32bit floats are commonly stored in memory. This means if you rely on float types while working with tiny fractions or large numbers, your calculations can end up between tiny fractions to several trillion off.

Источник

Сложение двух чисел с плавающей запятой без потери точности

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

то не кажется ли вам, что в результате его выполнения получится, что t=0? С точки зрения привычной математики действительных чисел это и правда так, а вот с точки зрения двоичной арифметики с плавающей запятой в переменной t будет кое-что другое. Там будет то, что спасает нас от потери точности при сложении чисел php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойи php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой. Кого интересует данная тема, прошу под кат.

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

По моей традиции текстовая статья дублируется на видео. По содержанию текст и видео идентичны.

Читателю статьи для её восприятия нужно понимать способ представления двоичных чисел с плавающей запятой в формате IEEE-754 и понимать, почему, например, 0,1+0,2≠0,3, но если такого навыка у вас по каким-то причинам нет, но вы хотели бы его приобрести, то прошу обратить внимание на список источников конце статьи, на пункты [1] и 4.

Итак, у нас следующая проблема. Сумма двух чисел с плавающей запятой c=a+b может вычисляться с некоторой ошибкой. Знаменитый на весь интернет пример 0,3 ≠ 0,2+0,1 хорошо это показывает. Наша задача в том, чтобы всё-таки складывать числа без этой ошибки. То есть сделать так, чтобы мы могли как-то сложить 0,2+0,1 и хоть в каком-то виде знать точный результат. В каком смысле точный, если даже исходные числа 0,1 и 0,2 не имеют точного представления в формате IEEE-754? Вот сейчас и поясню.

Начнём с того, что чисел 0,1 и 0,2 в двоичной арифметике с плавающей запятой быть не может, а наиболее близкие к ним значения для типа данных double (число удвоенной точности binary64, так его называют в Стандарте IEEE-754) следующие:

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

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

К сожалению, это данность, от неё никуда не уйти, если хранить числа в типе данных double (или любом другом типе фиксированного размера из Стандарта IEEE-754). Но проблема, которую мы решаем, другая. Нам нужно эти два числа, наиболее близких к 0,1 и 0,2, сложить так, чтобы получить результат без погрешности. То есть чтобы в результате сложения иметь число

К сожалению, если просто написать код s=0.1+0.2, то мы получим кое-что другое, а именно

что отличается от правильного ответа ровно на

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

«Подумаешь, — скажете вы, — десять в минус семнадцатой! Мне чтобы пиксель на экран вывести такая погрешность не помеха!». И будете правы. Задача точного вычисления каких-либо выражений относится к очень узкой области Computer Science, связанной с разработкой математических библиотек для языков программирования. Когда вы вызываете какую-нибудь функцию из такой библиотеки, то можете и не подозревать, что в основе её работы лежит труд десятков и сотен человек, выполняемый на протяжении десятилетий, а работает такая функция правильно благодаря совершенно неочевидным алгоритмам… Вот я и хотел бы приоткрыть для вас этот удивительный мир, для чего и пишу такие научно-популярные статьи.

Зачем может понадобиться точное сложение двух чисел? Приложений много, но вот одно из них, которое будет понятно всем. Если вы хотите сложить все числа из массива X[N] и сделаете это вот так:

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

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

У нас есть два числа php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойи php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойтипа double. Нам не важно, что эти числа php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойи php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойуже содержат в себе какую-то погрешность, полученную, например, в результате конвертирования десятичной строки в формат IEEE-754. Важно, что они сейчас перед нами, а их предыстория нас не интересует. Нужно сложить эти два числа c=a+b так, чтобы в результате сложения не возникло погрешности.

Но ведь это невозможно!

Да, это невозможно, спасибо что дочитали, до новых встреч 🙂

Шучу, конечно. Это невозможно только если мы храним результат сложения в одной переменной того же типа данных. Но вспомните пример выше. У нас была переменная s, и переменная t. Причём s была округлённым результатом сложения a+b, а t была равна разности этого округлённого результата и точного значения суммы. При этом выполняется равенство s+t=a+b.

И вот хорошая новость состоит в том, друзья, что такое представление суммы a+b в виде суммы s+t можно выполнить всегда (если a+b≠∞)! Если s=a+b оказывается точно-представимым значением в типе double, то очевидно, что t=0. Если это не так, то t будет равно некоторому очень маленькому (по сравнению с s) числу, которое является точно-представимым. Итак, вот оно, фундаментальное свойство суммы чисел с плавающей запятой: ошибка округления в результате суммирования чисел типа double всегда будет точно-представимым числом типа double! А это означает, что пара чисел (s, t) всегда даёт нам возможность сохранить сумму чисел a+b «как бы» без погрешности. Да, мы вынуждены хранить две переменные вместо одной, но прелесть в том, что их всего две, а не больше.

Теперь опишем это свойство на математическом языке. Введём обозначение RN(x) – это результат приведения произвольного вещественного числа x к типу данных double по правилу округления round-nearest-ties-to-even, то есть это округление к ближайшему, но в случае равного удаления от двух ближайших к тому, у которого последний бит мантиссы равен нулю (чётный). Именно этот режим работает почти везде по умолчанию, то есть если вы не знаете, о чём я сейчас говорю, то в вашем процессоре на 100% работает именно этот режим, так что не беспокойтесь.

Пусть a и b – числа типа double. Пусть |a|≥|b| и RN(a+b) ≠ ∞. Тогда следующий алгоритм

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

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

Прямоугольник олицетворяет переменную с плавающей запятой фиксированного размера, поэтому прямоугольники, помеченные символами «а» и «b» имеют одинаковый размер, но php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойсмещён относительно php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойвправо, потому что у нас есть условие: |a|≥|b|. Третий прямоугольник «Точное а+b» — это некоторое промежуточное значение, которое может иметь больший размер, чем размер переменных php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойи php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой, поэтому оно будет округлено, и «хвостик», вылезающий за границы нашего типа данных, будет отброшен. Таким образом, мы переходим к четвёртому прямоугольнику «Округлённое a+b», это и есть наше значение s, полученное в первой строке алгоритма. Если теперь из s снова вычесть php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой, то останется только «b без хвостика». Это делается во второй строке алгоритма. Теперь мы хотим получить «хвостик» от php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой, и это делается в третьей строке алгоритма, когда из исходной переменной php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкоймы вычитаем «b без хвостика». Остаётся «хвостик», это и есть переменная t, которая показывает ту самую погрешность, которая возникла в ходе округления. При этом из данных рассуждений видно, что такой «хвостик» всегда будет умещаться в одну переменную, потому что он не может превышать размера переменной php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой. В крайнем случае, когда смещение php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойотносительно php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкойбудет настолько большим, что s=RN(a+b)=a, то в этом случае, очевидно, z=0 и t=b. Скучный рисунок на эту тему я вам предлагаю изобразить самостоятельно.

Если вы не находите картинки убедительными для себя, то в книге [1, раздел 4.3.1, теорема 4.3] есть строгое математическое доказательство, но оно не умещается в формат научно-популярной статьи. Его краткая суть в том, что в нём показано почему в строках 2 и 3 алгоритма не будет выполняться округления, то есть эти выражения вычисляются точно, а если так, значит t=b-z=b-(s-a) = (a+b)-s в точности, что нам как раз и нужно: t является разностью между реальной суммой a+b и её округлённым значением s.

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

Пример 1.

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

Пояснение. Реальное значение a+b=2 53 +1, однако в типе данных double младший бит, равный единице, не влезет в 52 бита дробной части мантиссы, по какой причине будет выброшен при округлении, но переменная t «подхватит» его и сохранит в качестве погрешности, а переменная s сумеет сохранить только 2 53 ровно. Легко видеть, что s+t будет равно 2 53 +1.

Пример 2.

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

Пояснение выполнено в двоичном коде.

Я не говорил этого явно, но алгоритм работает даже когда b

Источник

Числа с плавающей точкой

Числа с плавающей точкой (также известные как «float», «double», или «real») могут быть определены следующими синтаксисами:

Размер числа с плавающей точкой зависит от платформы, хотя максимум, как правило составляет

1.8e308 с точностью около 14 десятичных цифр (64-битный IEEE формат).

Точность чисел с плавающей точкой

Числа с плавающей точкой имеют ограниченную точность. Хотя это зависит от операционной системы, в PHP обычно используется формат двойной точности IEEE 754, дающий максимальную относительную ошибку округления порядка 1.11e-16. Неэлементарные арифметические операции могут давать большие ошибки, и, разумеется, необходимо принимать во внимание распространение ошибок при совместном использовании нескольких операций.

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

«Простое» объяснение можно найти в » руководстве по числам с плавающей точкой, которое также называется «Why don’t my numbers add up?» («Почему мои числа не складываются?»)

Преобразование в число с плавающей точкой

Информацию о преобразовании строк в числа с плавающей точкой смотрите в разделе Преобразование строк в числа. Для значений других типов преобразование будет сначала осуществлено в в integer и затем в число с плавающей точкой. Дополнительную информацию смотрите в разделе Преобразование к целому. Начиная с версии PHP 5, при преобразовании объекта к числу с плавающей точкой выводится замечание об ошибке.

Сравнение чисел с плавающей точкой

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

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

= 1.23456789 ;
$b = 1.23456780 ;
$epsilon = 0.00001 ;

Источник

Всё, точка, приплыли! Учимся работать с числами с плавающей точкой и разрабатываем альтернативу с фиксированной точностью десятичной дроби

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

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

Куда уплывает точка

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

Мантисса — это, по сути, число, записанное без точки. Экспонента — это степень, в которую нужно возвести некое число N (как правило, N = 2), чтобы при перемножении на мантиссу получить искомое число (с точностью до разрядности мантиссы). Выглядит это примерно так:

Чтобы избежать неоднозначности, считается, что 1 = 4 503 599 627 370 496 и спокойно вмещает в себя все 32-разрядные целые, давая сбой только на действительно больших 64-разрядных целых (19 десятичных знаков), где погрешность в сотнях единиц уже, как правило, несущественна. Если же нужна большая точность, то мы в данной статье обязательно в этом поможем.

Теперь что касается экспоненты. Это обычное бинарное представление целого числа, в которое нужно возвести 10, чтобы при перемножении на мантиссу в нормализованном виде получить исходное число. Вот только в стандарте вдобавок ввели смещение, которое нужно вычитать из бинарного представления, чтобы получить искомую степень десятки (так называемая biased exponent — смещенная экспонента). Экспонента смещается для упрощения операции сравнения, то есть для одинарной точности берется значение 127, а для двойной 1023. Все это звучит крайне сложно, поэтому многие пропускают главу о типе с плавающей точкой. А зря!

Примерное плаванье

Чтобы стало чуточку понятнее, рассмотрим пример. Закодируем число 640 (= 512 + 128) в бинарном виде как вещественное число одинарной точности:

Задание на дом: разобраться в двоичной записи следующих констант: плюс и минус бесконечность (INF — бесконечность), ноль, минус ноль и число-не-число (NaN — not-a-number).

За буйки не заплывай!

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

Другое дело проблема точности. Жалкие 23 бита под мантиссу дают погрешность уже на 8-м знаке после запятой. Для чисел с двойной точностью ситуация не столь плачевная, но и 15 десятичных знаков очень быстро превращаются в проблему, если, например, при обработке данных требуется 6 фиксированных знаков после точки, а числа до точки достаточно большие, под них остается всего лишь 9 знаков. Соответственно, любые многомиллиардные суммы будут давать значительную погрешность в дробной части. При большой интенсивности обработки таких чисел могут пропадать миллиарды евро, просто потому, что они «не поместились», а погрешность дробной части суммировалась и накопила огромный остаток неучтенных данных.

Если бы это была только теория! На практике не должно пропадать даже тысячной доли цента, погрешность всех операций должна быть строго равна нулю. Поэтому для бизнес-логики, как правило, не используют C/C++, а берут C# или Python, где в стандартной библиотеке уже встроен тип Decimal, обрабатывающий десятичные дроби с нулевой погрешностью при указанной точности в десятичных знаках после запятой. Что же делать нам, программистам на C++, если перед нами стоит задача обработать числа очень большой разрядности, при этом не используя высокоуровневые языки программирования? Да то же, что и обычно: заполнить пробел, создав один небольшой тип данных для работы с десятичными дробями высокой точности, аналогичный типам Decimal высокоуровневых библиотек.

Добавим плавающей точке цемента

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

Сегодня в учебных целях мы рассмотрим, как создать представление вещественных чисел с гарантированной точностью до 18 знаков после точки. Это достигается простым комбинированием двух 64-разрядных целых для целой и дробной части соответственно. В принципе, никто не мешает вместо одного числа для каждой из компонент взять массив значений и получить полноценную «длинную» арифметику. Но будет более чем достаточно сейчас решить проблему точности, дав возможность работать с точностью по 18 знаков до и после запятой, зафиксировав точку между двумя этими значениями и залив ее цементом.

Отсыпь и мне децимала!

Сначала немного теории. Обозначим наше две компоненты, целую и дробную часть числа, как n и f, а само число будет представимо в виде

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

Операции с типом десятичной дроби

Разумеется, тип числа с повышенной точностью будет бесполезен без арифметических операций. Сложение реализуется сравнительно просто:

NB: здесь и далее все записи в форме 1e — целые числа.

Здесь [n] — это получение целой части числа, а — получение дробной части. Все бы хорошо, но вспоминаем про ограничение целых чисел. Значение 1e+18 уже близко к грани значений беззнакового 64-битового целого типа uint64_t (потому мы его и выбрали), но нам никто не мешает чуточку упростить выражение, чтобы гарантированно оставаться в границах типа, исходя из начальных условий:

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

Введем матрицу для упрощения вычисления умножения:

Здесь мы опускаем слагаемое A44 div 10 18 просто потому, что оно равно нулю. Разумеется, перед каждым сложением стоит проверить, не выйдем ли мы за пределы MAX_INT64. К счастью, мы можем оперировать беззнаковым типом uint64_t для всех компонент матрицы и для промежуточного результата. Все, что нужно будет сделать в конце, — это определить знак результата se = sa xor sc и для отрицательного числа поправить целую и дробную часть: целую уменьшить на единицу, дробную вычесть из единицы. Вот, в общем, и все умножение, главное — быть очень аккуратным. С ассемблером все на порядок проще, но этот материал выходит за рамки Академии C++.

Алгоритм деления без регистрации и СМС

Для упрощения рассмотрим нахождение обратного числа для положительного x. Если хотя бы одна из компонент x равна нулю (но не обе сразу), вычисления сильно упрощаются. Если a = 0, то:

Для более общего случая, когда x содержит ненулевые дробную и целую части, в этом случае уравнение сводится к следующему:

Теперь нужно найти максимальную степень 10, которая будет не больше a, и итерационно выполнять следующее действие:

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

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

Преобразования типов

Мы знаем и умеем достаточно, чтобы теперь превратить расплывчатые float и double в наш новенький decimal.

Здесь 103 является, по сути, той погрешностью, за которой double перестает быть точным. При желании погрешность можно еще уменьшить, здесь 10 18-15 нужно для наглядности изложения. Нормализация после преобразования нужна будет все равно, поскольку точно double заведомо ниже даже дробной части decimal. Кроме того, нужно учитывать случай, когда double выходит за пределы int64_t, при таких условиях наш decimal не сможет правильно преобразовать целую часть числа.

Все целые числа преобразовываются в decimal без проблем, просто инициализируя поле m_integral. Преобразование в обратную сторону для целых чисел также будет просто возврат m_integral, можно добавить округление m_fractional.

Преобразование из decimal в double и float сводится к вышеуказанной формуле:

Отдельно стоит рассмотреть преобразование в строку и из строки. Целочисленная часть, по сути, преобразуется в строку как есть, после этого остается только вставить decimal separator и вывести дробную часть как целое, отбросив завершающие нули. Также можно ввести поле «точность» m_precision и записывать в строку лишь указанное в нем число десятичных знаков.

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

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

GITHUB

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

Не уплывай, и точка!

В заключение скажу лишь то, что подобный тип в C/C++ может появиться в весьма специфической задаче. Как правило, проблемы чисел с большой точностью решаются языками типа Python или C#, но если уж понадобилось по 15–18 знаков до запятой и после, то смело используй данный тип.

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

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

php сложение чисел с плавающей точкой. Смотреть фото php сложение чисел с плавающей точкой. Смотреть картинку php сложение чисел с плавающей точкой. Картинка про php сложение чисел с плавающей точкой. Фото php сложение чисел с плавающей точкой

Впервые опубликовано в журнале Хакер #192.
Автор: Владимир Qualab Керимов, ведущий С++ разработчик компании Parallels

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *