![](https://habrastorage.org/getpro/habr/upload_files/c31/632/155/c31632155acb0382f55976964c51a569.png)
С вашим языком программирования все в порядке — он просто производит вычисления с плавающей запятой. Изначально компьютеры могут хранить только целые числа, так что им нужен какой-то способ представления десятичных чисел. Это представление не совсем точное. Именно поэтому, чаще всего, 0.1 + 0.2 != 0.3.
ИТ-эксперт Эрик Уиффин, директор по инжинирингу компании Devetry, провел любопытный эксперимент: сравнил вывод в разных языках программирования при вычислениях с плавающей запятой. В рамках опыта автор продемонстрировал специфику выполнения одной и той же математической операции в нескольких десятках языков.
Предлагаем хабрасообществу наш перевод этого материала. Обращаем ваше внимание, что позиция автора не всегда может совпадать с мнением МойОфис.
Если вы используете стандартную десятичную систему счисления, то несократимая обыкновенная дробь представляется конечной десятичной дробью только в том случае, когда ее знаменатель содержит в разложении на простые множители только числа 2 и 5 (т.е. только простые делители числа 10). Таким образом, 1/2, 1/4, 1/5, 1/8 и 1/10 могут быть точно выражены, поскольку все знаменатели используют простые множители числа 10. Напротив, 1/3, 1/6, 1/7 и 1/9 — периодические десятичные дроби, потому что в их знаменателях используется простой множитель 3 или 7.
В двоичном формате (или с основанием 2) единственным простым делителем является 2, поэтому вы можете точно выразить только те дроби, знаменатель которых имеет 2 в качестве простого делителя. В двоичном формате 1/2, 1/4, 1/8 будут точно выражены в виде десятичных дробей, а 1/5 или 1/10 будут периодическими десятичными дробями. Таким образом, 0,1 и 0,2 (1/10 и 1/5), будучи чистыми десятичными числами в десятичной системе, являются периодическими десятичными числами в системе с основанием 2, которую использует компьютер. Если вы выполняете вычисления с их участием, вы получаете остатки, которые переносятся, когда вы конвертируете «компьютерное» число с основанием 2 (двоичное) в более удобочитаемое представление с основанием 10.
Ниже приведены несколько примеров печати .1 + .2
в стандартный вывод на разных языках. Все примеры представлены в формате «Язык — Код — Результат».
PowerShell по умолчанию использует тип double, но поскольку он работает на .NET, то имеет те же типы, что и C#. Благодаря этому можно напрямую использовать тип Decimal [decimal]
, указав имя типа либо посредством суффикса d
.
Подробнее об этом читайте ниже, в разделе про C#.
![](https://habrastorage.org/getpro/habr/upload_files/0be/60a/e64/0be60ae646658174114f82fc8787fe85.png)
![](https://habrastorage.org/getpro/habr/upload_files/80e/254/207/80e254207a2ad0baae65d8b2eaddb254.png)
По умолчанию точность вывода APL — 10 значимых цифр. Установка значения 17 для ⎕PP
выдает ошибку, однако все еще верно (1), что 0.3 = 0.1 + 0.2
, поскольку допуск сравнения по умолчанию составляет около 10^-14 . Установка ⎕CT
на 0 выдает неравенство. Dyalog APL также поддерживает 128-битные десятичные числа (активируется установкой представления с плавающей запятой, ⎕FR
, на 1287, т. е. 128-битным десятичным числом), где даже установка допусков десятичного сравнения (⎕DCT
) на ноль все еще делает уравнение верным. Убедитесь в этом здесь! В NARS2000 доступны числа с плавающей точкой с множественной точностью, рациональные числа с неограниченной точностью и комплексные интервальные вычисления с кругами (ball arithmetic).
![](https://habrastorage.org/getpro/habr/upload_files/7d2/967/095/7d2967095add68261fa9d604958e9619.png)
![](https://habrastorage.org/getpro/habr/upload_files/084/6d5/cd9/0846d5cd94da7e598878be7f2ee7bc75.jpg)
![](https://habrastorage.org/getpro/habr/upload_files/138/cba/22f/138cba22fda2a23b03a51942c4c8fde3.png)
![](https://habrastorage.org/getpro/habr/upload_files/150/cf6/4fd/150cf64fd898b01c67ccdd26883b46b1.png)
C# поддерживает 128-битные десятичные числа с точностью до 28-29 значащих цифр. Однако их диапазон меньше, чем у типов с плавающей запятой одинарной и двойной точности. Десятичные литералы обозначаются суффиксом m
.
![](https://habrastorage.org/getpro/habr/upload_files/68b/453/4b8/68b4534b872dcbcd32e150615ac1bd70.gif)
![](https://habrastorage.org/getpro/habr/upload_files/152/8b0/011/1528b0011796786abc149ceb46f276a8.png)
Clojure поддерживает произвольную точность и соотношения. (+ 0,1M 0,2M)
возвращает 0,3M
, в то время как (+ 1/10 2/10)
возвращает 3/10
.
![](https://habrastorage.org/getpro/habr/upload_files/7d0/6eb/123/7d06eb12330f016bfa383cbe87878926.png)
![](https://habrastorage.org/getpro/habr/upload_files/360/e47/cb9/360e47cb9354d3f16367d02a24024657.png)
Спецификация CL на самом деле не требует даже чисел с основанием 2 с плавающей запятой (не говоря уже о 32-битных одинарных и 64-битных двойных), но все высокопроизводительные реализации, похоже, используют числа с плавающей запятой IEEE с обычными размерами. Это было протестировано, в частности, на SBCL и ECL.
![](https://habrastorage.org/getpro/habr/upload_files/82d/073/c77/82d073c774d73413880272c9d9cedbe8.png)
![](https://habrastorage.org/getpro/habr/upload_files/f80/070/7e3/f800707e3082f4cc02ffdc7bca19829a.png)
![](https://habrastorage.org/getpro/habr/upload_files/0f1/a91/d39/0f1a91d396c683f8d15a1f75710d973e.png)
![](https://habrastorage.org/getpro/habr/upload_files/c5f/5ee/7d8/c5f5ee7d87e24dc0a2aecd1e8529a665.png)
![](https://habrastorage.org/getpro/habr/upload_files/ed7/57b/9ca/ed757b9ca010e615eac91457928244e3.png)
![](https://habrastorage.org/getpro/habr/upload_files/aad/a07/900/aada07900f17130406a0642688cf9ff6.png)
![](https://habrastorage.org/getpro/habr/upload_files/9a7/0f5/d92/9a70f5d925334627c683efc5a0eaa317.png)
Elvish использует тип double
языка Go для числовых операций.
![](https://habrastorage.org/getpro/habr/upload_files/dbb/410/40d/dbb41040df1472a903891c4008e2178d.png)
![](https://habrastorage.org/getpro/habr/upload_files/392/682/2dc/3926822dc2a7ae39daff13f4ecd62093.png)
![](https://habrastorage.org/getpro/habr/upload_files/ed3/b95/d41/ed3b95d4164295ace703f8d5bc07989d.gif)
![](https://habrastorage.org/getpro/habr/upload_files/c92/54e/66e/c9254e66e652e4394440270784805077.png)
![](https://habrastorage.org/getpro/habr/upload_files/a4c/515/87d/a4c51587dd615e1c27c8fb92dbe86a02.png)
Если вам нужны действительные числа, пакеты типа exact-real дадут вам правильный ответ.
![](https://habrastorage.org/getpro/habr/upload_files/474/348/557/474348557608fdecfa5fc7afc37bc924.png)
![](https://habrastorage.org/getpro/habr/upload_files/7c7/246/1fa/7c72461fa50a2fe8fe49fc9dbcb642c8.png)
В Gforth 0
означает ложь, а -1
означает истину. Первый пример выводит 0,3
, но этот результат не равен фактическому значению 0,3
.
![](https://habrastorage.org/getpro/habr/upload_files/ee1/b2a/aa2/ee1b2aaa2ae2171897f33c93cc104a2a.gif)
Числовые константы Go имеют произвольную точность.
![](https://habrastorage.org/getpro/habr/upload_files/69d/ef7/296/69def72963b1e68524d80a73afb6d486.png)
Буквенные десятичные значения в Groovy являются экземплярами java.math.BigDecimal.
![](https://habrastorage.org/getpro/habr/upload_files/e6e/998/e40/e6e998e404b4c991b5e75150629504ca.png)
![](https://habrastorage.org/getpro/habr/upload_files/416/ff3/8ba/416ff38ba3be16239f8648eca87237f6.png)
![](https://habrastorage.org/getpro/habr/upload_files/f03/47b/f6d/f0347bf6d2f21dce704683430f4cd641.png)
![](https://habrastorage.org/getpro/habr/upload_files/738/8b9/883/7388b98837bc485f181eb52960abc5b1.png)
Java имеет встроенную поддержку чисел произвольной точности с использованием класса BigDecimal.
![](https://habrastorage.org/getpro/habr/upload_files/a27/880/0c4/a278800c4324bb9f552fc7af1f945f74.jpg)
Библиотека decimal.js предоставляет тип Decimal произвольной точности для JavaScript.
![](https://habrastorage.org/getpro/habr/upload_files/f93/067/003/f93067003c092ed5ef172bc0091c9a42.png)
Julia имеет встроенную поддержку рациональных чисел, а также встроенный тип данных BigFloat произвольной точности.
![](https://habrastorage.org/getpro/habr/upload_files/c5c/95a/981/c5c95a9816e29abb16254f50eed544cb.png)
![](https://habrastorage.org/getpro/habr/upload_files/1af/216/c3c/1af216c3cf304bc9a3d8049853c4c2aa.png)
![](https://habrastorage.org/getpro/habr/upload_files/04f/49e/046/04f49e046cb70749919949d929e88e48.png)
![](https://habrastorage.org/getpro/habr/upload_files/b82/ae3/886/b82ae388628a006239b6d722667905a6.png)
![](https://habrastorage.org/getpro/habr/upload_files/594/f73/95c/594f7395c05041dde8e8fcf96d6be341.png)
Спецификация схемы содержит понятие точности.
![](https://habrastorage.org/getpro/habr/upload_files/0db/6ac/cfd/0db6accfdee7267e8170e3bbea9677ef.png)
В языке Mathematica есть довольно продуманный внутренний механизм для работы с числовой точностью, и она поддерживает произвольную точность.
По умолчанию для исходных данных 0,1
и 0,2
в этом примере используется MachinePresicion. При обычном значении MachinePrecision в 15,9546
цифр, 0,1 + 0,2
фактически имеет [FullForm][4] 0,300000000000000004
, но выводится как 0,3
.
Mathematica поддерживает рациональные числа: 1/10 + 2/10
равно 3/10
(что имеет FullForm
Rational[3, 10]
).
![](https://habrastorage.org/getpro/habr/upload_files/fc1/6ca/b09/fc16cab09e73d0a3260d9f2e357470f1.png)
![](https://habrastorage.org/getpro/habr/upload_files/864/720/7a7/8647207a7338aacdf44885eeb0e283ee.png)
![](https://habrastorage.org/getpro/habr/upload_files/692/e16/186/692e16186d3375ff7715ef44c32882cd.png)
![](https://habrastorage.org/getpro/habr/upload_files/804/0f4/a95/8040f4a9514b3cacace3411f1318b706.png)
![](https://habrastorage.org/getpro/habr/upload_files/874/94a/097/87494a097cc157dbc9728b29b61267ea.png)
PHP echo
преобразует 0.300000000000000004441
в строку и сокращает ее до «0.3». Чтобы добиться желаемого результата с плавающей запятой, отрегулируйте параметр точности: ini_set("precision", 17)
.
![](https://habrastorage.org/getpro/habr/upload_files/458/571/5fc/4585715fce8f97ab7c2815f5ba555c42.gif)
Добавление примитивов с плавающей запятой только кажется верным для вывода, потому что не все 17 цифр выводятся по умолчанию. Базовый пакет Math::BigFloat позволяет выполнять операции с плавающей запятой с произвольной точностью, никогда не используя числовые примитивы.
![](https://habrastorage.org/getpro/habr/upload_files/864/2c6/626/8642c6626752079da5fa2e744c63d996.png)
Вам нужно загрузить файл «frac.min.l».
![](https://habrastorage.org/getpro/habr/upload_files/ccf/3b4/23c/ccf3b423c4f368b8505bcca0abd182ea.png)
PostgreSQL рассматривает десятичные литералы как числа произвольной точности с фиксированной точкой. Для получения чисел с плавающей запятой требуется явное приведение типов.
PostgreSQL 11 и более ранние версии выдает результат 0.3 для запроса SELECT 0.1::float + 0.2::float;
, но результат округляется только для отображения, под капотом же у нас все еще 0.300000000000000004
.
В PostgreSQL 12 поведение по умолчанию для текстового вывода чисел с плавающей запятой было изменено с более удобочитаемого округленного формата на максимально точный формат. Формат можно настроить с помощью параметра конфигурации extra_float_digits.
![](https://habrastorage.org/getpro/habr/upload_files/a79/317/b85/a79317b8524397e5c733656c0ac6d60a.png)
![](https://habrastorage.org/getpro/habr/upload_files/c93/e90/9be/c93e909befa33811413e2ed754b98f11.png)
Pyret имеет встроенную поддержку как рациональных чисел, так и чисел с плавающей запятой. Числа, написанные как обычно, считаются точными. Напротив, RoughNums представлены плавающими точками и написаны с префиксом ~
, что указывает на то, что они не являются точными результатами. Пользователь, увидевший результат вычислений ~0,30000000000000004
, знает, что к этому значению нужно относиться скептически. RoughNums нельзя прямо сравнивать для равенства; их можно сравнивать только с заданным допуском.
![](https://habrastorage.org/getpro/habr/upload_files/d7c/d3e/a6b/d7cd3ea6b3b0ecdf89de39cf0f8c8178.gif)
В Python 2 оператор print
преобразует 0,300000000000000004
в строку и сокращает ее до «0,3». Чтобы добиться желаемого результата с плавающей запятой, используйте print repr(.1 + .2)
. Это было исправлено в Python 3 (см. ниже).
![](https://habrastorage.org/getpro/habr/upload_files/3cc/457/101/3cc457101bd24c81b3e758eab58ce592.gif)
Python (как 2, так и 3) поддерживает десятичные вычисления с модулем decimal и истинные рациональные числа с модулем дробей.
![](https://habrastorage.org/getpro/habr/upload_files/034/08a/7c9/03408a7c91d403ab0498c168d500679e.png)
![](https://habrastorage.org/getpro/habr/upload_files/248/7f2/dee/2487f2dee4add881422c519cea42b7b5.png)
![](https://habrastorage.org/getpro/habr/upload_files/5c8/660/d0a/5c8660d0a4f695aaac0dc8782a09a1f1.png)
Raku по умолчанию использует рациональные числа, поэтому .1
хранится примерно так: { numerator => 1, denominator => 10 }
. Чтобы в реальности вызвать такое поведение, вы должны заставить числа иметь тип Num (double в терминах C) и использовать базовую функцию вместо функций sprintf
или fmt
(поскольку в этих функциях есть ошибка, которая ограничивает точность вывода).
![](https://habrastorage.org/getpro/habr/upload_files/bb9/e19/83a/bb9e1983ae58f22b45df20c8a85f051b.png)
![](https://habrastorage.org/getpro/habr/upload_files/64e/5e6/224/64e5e62246e62909dcc261893fcbdb07.png)
Ruby напрямую поддерживает рациональные числа в синтаксисе версии 2.1 и новее. Для более старых версий используйте Rational. В Ruby также есть библиотека для работы с десятичными знаками: BigDecimal.
![](https://habrastorage.org/getpro/habr/upload_files/a1c/f95/59e/a1cf9559e369bed7735975bd91015eb1.gif)
В Rust есть поддержка рациональных чисел из num crate.
![](https://habrastorage.org/getpro/habr/upload_files/f60/5a9/064/f605a90644bd8d6a422da84910add58b.png)
SageMath поддерживает различные поля для вычислений: вещественные числа произвольной точности, RealDoubleField, Ball Arichmetic, рациональные числа и т. д.
![](https://habrastorage.org/getpro/habr/upload_files/a14/d27/1fd/a14d271fd521b26c66d3819ace7fa23a.gif)
![](https://habrastorage.org/getpro/habr/upload_files/53c/920/457/53c920457b39c62679d820c560e05c2e.png)
В большинстве операций Smalltalk по умолчанию использует дроби; на самом деле стандартное деление приводит к дробям, а не к числам с плавающей запятой. Squeak и аналогичные Smalltalk предоставляют «масштабированные десятичные числа», которые позволяют использовать вещественные числа с фиксированной точкой (s
-суффикс указывает точные разряды).
![](https://habrastorage.org/getpro/habr/upload_files/393/f0b/ce5/393f0bce585e1e3dfee2ae8bde17307d.png)
Swift поддерживает десятичные вычисления с модулем Foundation.
![](https://habrastorage.org/getpro/habr/upload_files/367/e4d/cc7/367e4dcc7604651af2a82ba747bddc8a.png)
![](https://habrastorage.org/getpro/habr/upload_files/23c/afd/8ee/23cafd8ee06a3eb6f842a4987de7e6ac.png)
![](https://habrastorage.org/getpro/habr/upload_files/9a0/ea8/718/9a0ea8718619029075483cc651c0f4a6.png)
![](https://habrastorage.org/getpro/habr/upload_files/693/12e/185/69312e185e87b8981c5ba47d73606e8c.gif)
Добавление символа типа идентификатора #
к любому идентификатору приводит к тому, что он становится Double.
![](https://habrastorage.org/getpro/habr/upload_files/d89/568/30a/d8956830a80e2b63eb26fbd0e00aeb5f.png)
Смотрите демо.
![](https://habrastorage.org/getpro/habr/upload_files/000/6b0/f26/0006b0f264b86097ab783e1dab53973c.png)
![](https://habrastorage.org/getpro/habr/upload_files/261/d72/624/261d72624eb4a8ccd566e1026b5b1c82.png)
![](https://habrastorage.org/getpro/habr/upload_files/9de/b62/85d/9deb6285ddd84b09bc8d6557c0fa0587.png)
![](https://habrastorage.org/getpro/habr/upload_files/081/131/91b/08113191bc95ce2bb4c9ae70c02ec5e3.png)
***
Будем рады узнать в комментариях ваше мнение об описанном опыте с вычислениями и его результатах. Впереди — еще больше полезных переводов и материалов с ИТ-экспертизой от специалистов МойОфис. Следите за нашими новостями и блогом на Хабре!
Комментарии (17)
Myclass
18.08.2022 16:21+4Вообще-то это вот эта страница Эрика.
https://0.30000000000000004.com
И ещё — всего две функции — плюс н минус показаны. Где умножение, деление, где сравнение, т.к. с числами с плавающей запятой разлижные правила математики не работают, например X + Y иногда не равно Y + X. Не говоря уже об Y * (B * C) и (A * B) * C.
Так-же и правило малых чисел стоит упомянуть, как например этот пример и что из него будет:
1.5 × 10^20 + 100.5
Очень часто проверка на ноль может не работать.ShadowTheAge
18.08.2022 16:31почти наверняка там где получилось 0.30..04 все операции будут работать одинаково
А еще сложение в числах с плавающей запятой коммутативно, т.е. X + Y всегда равно Y + X (но не ассоциативно) (ну и если не брать в расчет NaN+NaN)
dmitryvolochaev
18.08.2022 17:56Поэтому если надо просуммировать кучу чисел примерно одного порядка, не надо делать это наивным методом, т.е. объявить переменную и прибавлять к ней слагаемые по одному
Voland69
18.08.2022 16:37Проверка на ноль в мире float/double не работает ± всегда. Первое правило - задать достаточную в рамках задачи точность eps, в пределах которой два числа считаются равными. Правда тут начинаются интересности со сложением чисел с сильно разными порядками.
В самом интересном случае можно одним exe на двух компах получить в зоне "обычных" значение идентичные результаты, а в зоне, приближающейся к краю диапазона (~10^-20 для float) разницу в порядок-другой.
panteleymonov
18.08.2022 17:02+2Проверка на ноль в мире float/double не работает ± всегда.
Проверка на ноль работает всегда! Это компьютер. В первую очередь нужно уяснить для себя, что вы подразумеваете под этой проверкой на ноль. И когда требуется проверить именно на содержание нуля, а не бесконечно малого числа - это всегда работает.
Вторая проблема это когда вы задаете этот самый фиксированный EPS, то автоматически отсекаете возможный диапазон решений на более малый числах, когда текущая порядковая точность это позволяет. В этом и суть плавающего числа. А если вам нужен EPS, то подозреваю вам не плавающие числа нужны, а числа с фиксированной точкой.
dmitryvolochaev
18.08.2022 18:01+1Как говорится, конец немного предсказуем. Кто-нибудь знает язык, который бы для вычислений с плавающей запятой не использовал сопроцессор? Или хоть одну причину чтобы использовать такой язык?
ap1973
18.08.2022 22:12Все БД вовсе не используют плавающую запятую (возможность такая есть, обычно, но я не встречал в реальной жизни). У нас в системе приложены титанические усилия, что бы все вычисления проводились над типами с фиксированной точностью - ибо финансовые вычисления, и флоаты для них смерть. Вот и причина.
gnomeby
19.08.2022 04:41+1Ну БД просто по умолчанию старается в numeric, но float там есть, да и по примерам выше это видно.
Zara6502
19.08.2022 09:19+1мне кажется акцент статьи не туда смотрит, куда лучше объяснять всё на примерах того, как именно операции в "голове" у языка происходят. Например если вы хотите 0.1 умножить на целое, а потом прибавить опять дробное 0.3, то сразу вылезет проблема (в некоторых языках) с тем, что после умножения у вас будет целое и после сложения останется целое, то есть фактически 0.1 вы просто теряете, поэтому в разных языках формулу придется использовать по-разному или явно указывать тип, что-то вроде (double)(0.3*x)+0.1
Kyushu
19.08.2022 09:48Дроби это маленькая часть чисел с плавающей запятой. То, что ограничивается числами типа x.yz, легко решается путем перехода данных "от рублей к копейкам". Для операций со всем остальным приходится мириться, что не только данные представляются неточно, но и результат операций тоже вычисляется неточно. Ну, и конечно, a+b+c начинает зависеть от порядка вычислений.
main2stels
19.08.2022 16:34Был баг на проде, где из файла читали целое в строке 13 и парсили в double получалось 12.999… и потом уже приводили к int. Жуткая штука
jetyb1
19.08.2022 17:31В C# меня бесит, что стандартный вывод плавающих чисел идёт через запятую, а стандартный парсинг - через точку. Идиотизм.
saga111a
19.08.2022 20:29+1только сегодня столкнулся с "подарком" от вычислений с плавающей точкой.
Нужно было запускать расчеты толщины пластины с разной точность. Использован был numpy.import numpy as np np.arange(0.1, 0.5, 0.1) # array([0.1, 0.2, 0.3, 0.4]) np.arange(0.6, 1.1, 0.1) # array([0.6, 0.7, 0.8, 0.9, 1. , 1.1]) np.arange(0.6, 1.0, 0.1) # array([0.6, 0.7, 0.8, 0.9]) np.arange(1.6, 2.1, 0.1) # array([1.6, 1.7, 1.8, 1.9, 2. ]) np.arange(0.6, 1.2, 0.1) # array([0.6, 0.7, 0.8, 0.9, 1. , 1.1])
Строка 2 нормальное логичное поведение из парадигмы питона, не учитывать последний элемента
Строка 3, ....
Строка 4,кажется что разработчики ненавидят цифру 1
Строка 5, почему не совпадает с 3!?
Строка 6, почему совпадает с 3!?
Мне кажется 3 и 4й примеры работы прекрасны. Разработчики numpy похожу что в курсе и в документации об этом как бы сказано(мне кажется это повод удалить саму функцию или запретить использование для float, а не писать что поведение ну типа странное).
Да, понимаю что float не так прост, но почему библиотека которая позиционирует себя как что-то для работы с точными вычислениями, такое выдает!? И вы еще боритесь за написание кода высокой культуры? Сколько скрытых ошибок благодаря такому поведению кода можно ожидать?
raamid
Самое веселье начинается в шейдерах. Например, если говорить о WebGL, то там целое число high precision integer на десктопе 32 разрядное, 4 байтное, т.е. до 4,294,967,296 , а вот на мобильных устройствах целое является 24 разрядным, т.е. до 16,777,216. И нигде про это не написано, нащупал методом граблей.
PKav
А прямо задаваемых типов нет? В C ведь придумали uint32_t и подобные и избежали неоднозначности между архитектурами.
raamid
Прямо такого нет. В шейдерах задаются int & float. Кроме того, можно вначале шейдера объявить точность таким вот образом:
precision highp int;
precision highp float;
float a;
Это означает, что для типа int & float будет использоваться максимально возможная точность. Только вот на разном железе она разная. На десктопах это int32, на мобильных устройствах int24.