[Перевод] Вычисления с плавающей запятой: сравниваем вывод в разных языках

c31632155acb0382f55976964c51a569.png

С вашим языком программирования все в порядке — он просто производит вычисления с плавающей запятой. Изначально компьютеры могут хранить только целые числа, так что им нужен какой-то способ представления десятичных чисел. Это представление не совсем точное. Именно поэтому, чаще всего, 0.1 + 0.2!= 0.3.

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

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

Если вы используете стандартную десятичную систему счисления, то несократимая обыкновенная дробь представляется конечной десятичной дробью только в том случае, когда ее знаменатель содержит в разложении на простые множители только числа 2 и 5 (т.е. только простые делители числа 10). Таким образом, ½, ¼, 1/5, 1/8 и 1/10 могут быть точно выражены, поскольку все знаменатели используют простые множители числа 10. Напротив, ⅓, 1/6, 1/7 и 1/9 — периодические десятичные дроби, потому что в их знаменателях используется простой множитель 3 или 7.

В двоичном формате (или с основанием 2) единственным простым делителем является 2, поэтому вы можете точно выразить только те дроби, знаменатель которых имеет 2 в качестве простого делителя. В двоичном формате ½, ¼, 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#.

0be60ae646658174114f82fc8787fe85.png80e254207a2ad0baae65d8b2eaddb254.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).

7d2967095add68261fa9d604958e9619.png0846d5cd94da7e598878be7f2ee7bc75.jpg138cba22fda2a23b03a51942c4c8fde3.png150cf64fd898b01c67ccdd26883b46b1.png

C# поддерживает 128-битные десятичные числа с точностью до 28–29 значащих цифр. Однако их диапазон меньше, чем у типов с плавающей запятой одинарной и двойной точности. Десятичные литералы обозначаются суффиксом m.

68b4534b872dcbcd32e150615ac1bd70.gif1528b0011796786abc149ceb46f276a8.png

Clojure поддерживает произвольную точность и соотношения. (+ 0,1M 0,2M) возвращает 0,3M, в то время как (+ 1/10 2/10) возвращает 3/10.

7d06eb12330f016bfa383cbe87878926.png360e47cb9354d3f16367d02a24024657.png

Спецификация CL на самом деле не требует даже чисел с основанием 2 с плавающей запятой (не говоря уже о 32-битных одинарных и 64-битных двойных), но все высокопроизводительные реализации, похоже, используют числа с плавающей запятой IEEE с обычными размерами. Это было протестировано, в частности, на SBCL и ECL.

82d073c774d73413880272c9d9cedbe8.pngf800707e3082f4cc02ffdc7bca19829a.png0f1a91d396c683f8d15a1f75710d973e.pngc5f5ee7d87e24dc0a2aecd1e8529a665.pnged757b9ca010e615eac91457928244e3.pngaada07900f17130406a0642688cf9ff6.png9a70f5d925334627c683efc5a0eaa317.png

Elvish использует тип double языка Go для числовых операций.

dbb41040df1472a903891c4008e2178d.png3926822dc2a7ae39daff13f4ecd62093.pnged3b95d4164295ace703f8d5bc07989d.gifc9254e66e652e4394440270784805077.pnga4c51587dd615e1c27c8fb92dbe86a02.png

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

474348557608fdecfa5fc7afc37bc924.png7c72461fa50a2fe8fe49fc9dbcb642c8.png

В Gforth 0 означает ложь, а -1 означает истину. Первый пример выводит 0,3, но этот результат не равен фактическому значению 0,3.

e56bcf46925e5dec0a031782eb5adeda.png

Числовые константы Go имеют произвольную точность.

69def72963b1e68524d80a73afb6d486.png

Буквенные десятичные значения в Groovy являются экземплярами java.math.BigDecimal.

e6e998e404b4c991b5e75150629504ca.png416ff38ba3be16239f8648eca87237f6.pngf0347bf6d2f21dce704683430f4cd641.png7388b98837bc485f181eb52960abc5b1.png

Java имеет встроенную поддержку чисел произвольной точности с использованием класса BigDecimal.

a278800c4324bb9f552fc7af1f945f74.jpg

Библиотека decimal.js предоставляет тип Decimal произвольной точности для JavaScript.

f93067003c092ed5ef172bc0091c9a42.png

Julia имеет встроенную поддержку рациональных чисел, а также встроенный тип данных BigFloat произвольной точности.

c5c95a9816e29abb16254f50eed544cb.png1af216c3cf304bc9a3d8049853c4c2aa.png

См. справочную документацию.

04f49e046cb70749919949d929e88e48.pngb82ae388628a006239b6d722667905a6.png594f7395c05041dde8e8fcf96d6be341.png

Спецификация схемы содержит понятие точности.

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]).

fc16cab09e73d0a3260d9f2e357470f1.png8647207a7338aacdf44885eeb0e283ee.png692e16186d3375ff7715ef44c32882cd.png8040f4a9514b3cacace3411f1318b706.png87494a097cc157dbc9728b29b61267ea.png

PHP echo преобразует 0.300000000000000004441 в строку и сокращает ее до »0.3». Чтобы добиться желаемого результата с плавающей запятой, отрегулируйте параметр точности: ini_set("precision", 17).

4585715fce8f97ab7c2815f5ba555c42.gif

Добавление примитивов с плавающей запятой только кажется верным для вывода, потому что не все 17 цифр выводятся по умолчанию. Базовый пакет Math: BigFloat позволяет выполнять операции с плавающей запятой с произвольной точностью, никогда не используя числовые примитивы.

8642c6626752079da5fa2e744c63d996.png

Вам нужно загрузить файл «frac.min.l».

ccf3b423c4f368b8505bcca0abd182ea.png

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

PostgreSQL 11 и более ранние версии выдает результат 0.3 для запроса SELECT 0.1::float + 0.2::float;, но результат округляется только для отображения, под капотом же у нас все еще 0.300000000000000004.

В PostgreSQL 12 поведение по умолчанию для текстового вывода чисел с плавающей запятой было изменено с более удобочитаемого округленного формата на максимально точный формат. Формат можно настроить с помощью параметра конфигурации extra_float_digits.

a79317b8524397e5c733656c0ac6d60a.pngc93e909befa33811413e2ed754b98f11.png

Pyret имеет встроенную поддержку как рациональных чисел, так и чисел с плавающей запятой. Числа, написанные как обычно, считаются точными. Напротив, RoughNums представлены плавающими точками и написаны с префиксом ~, что указывает на то, что они не являются точными результатами. Пользователь, увидевший результат вычислений ~0,30000000000000004, знает, что к этому значению нужно относиться скептически. RoughNums нельзя прямо сравнивать для равенства; их можно сравнивать только с заданным допуском.

d7cd3ea6b3b0ecdf89de39cf0f8c8178.gif

В Python 2 оператор print преобразует 0,300000000000000004 в строку и сокращает ее до »0,3». Чтобы добиться желаемого результата с плавающей запятой, используйте print repr(.1 + .2). Это было исправлено в Python 3 (см. ниже).

3cc457101bd24c81b3e758eab58ce592.gif

Python (как 2, так и 3) поддерживает десятичные вычисления с модулем decimal и истинные рациональные числа с модулем дробей.

03408a7c91d403ab0498c168d500679e.png2487f2dee4add881422c519cea42b7b5.png5c8660d0a4f695aaac0dc8782a09a1f1.png

Raku по умолчанию использует рациональные числа, поэтому .1 хранится примерно так: { numerator => 1, denominator => 10 }. Чтобы в реальности вызвать такое поведение, вы должны заставить числа иметь тип Num (double в терминах C) и использовать базовую функцию вместо функций sprintf или fmt (поскольку в этих функциях есть ошибка, которая ограничивает точность вывода).

bb9e1983ae58f22b45df20c8a85f051b.png64e5e62246e62909dcc261893fcbdb07.png

Ruby напрямую поддерживает рациональные числа в синтаксисе версии 2.1 и новее. Для более старых версий используйте Rational. В Ruby также есть библиотека для работы с десятичными знаками: BigDecimal.

a1cf9559e369bed7735975bd91015eb1.gif

В Rust есть поддержка рациональных чисел из num crate.

f605a90644bd8d6a422da84910add58b.png

SageMath поддерживает различные поля для вычислений: вещественные числа произвольной точности, RealDoubleField, Ball Arichmetic, рациональные числа и т. д.

a14d271fd521b26c66d3819ace7fa23a.gif53c920457b39c62679d820c560e05c2e.png

В большинстве операций Smalltalk по умолчанию использует дроби; на самом деле стандартное деление приводит к дробям, а не к числам с плавающей запятой. Squeak и аналогичные Smalltalk предоставляют «масштабированные десятичные числа», которые позволяют использовать вещественные числа с фиксированной точкой (s-суффикс указывает точные разряды).

393f0bce585e1e3dfee2ae8bde17307d.png

Swift поддерживает десятичные вычисления с модулем Foundation.

367e4dcc7604651af2a82ba747bddc8a.png23cafd8ee06a3eb6f842a4987de7e6ac.png9a0ea8718619029075483cc651c0f4a6.png69312e185e87b8981c5ba47d73606e8c.gif

Добавление символа типа идентификатора # к любому идентификатору приводит к тому, что он становится Double.

d8956830a80e2b63eb26fbd0e00aeb5f.png

Смотрите демо.

0006b0f264b86097ab783e1dab53973c.png261d72624eb4a8ccd566e1026b5b1c82.png9deb6285ddd84b09bc8d6557c0fa0587.png08113191bc95ce2bb4c9ae70c02ec5e3.png

***

Будем рады узнать в комментариях ваше мнение об описанном опыте с вычислениями и его результатах. Впереди — еще больше полезных переводов и материалов с ИТ-экспертизой от специалистов МойОфис. Следите за нашими новостями и блогом на Хабре!

© Habrahabr.ru