Неправильное понимание арифметики с плавающей запятой и ее недостатков является основной причиной удивления и путаницы в программировании (рассмотрим количество вопросов о переполнении стека, относящихся к «числам, которые не складываются правильно»). Учитывая то, что многим программистам еще предстоит понять его последствия, он может внести много тонких ошибок (особенно в финансовое программное обеспечение). Что могут сделать языки программирования, чтобы избежать ошибок для тех, кто не знаком с концепциями, и в то же время предлагает свою скорость, когда точность не критична для тех, кто понимает концепции?
language-design
Адам Пейнтер
источник
источник
Ответы:
Вы говорите , что «особенно для финансового обеспечения», которая воспитывает одна из моих любимых мозолей: деньги не поплавок, что это ИНТ .
Конечно, это выглядит как поплавок. Там есть десятичная точка. Но это только потому, что вы привыкли к подразделениям, которые путают проблему. Деньги всегда приходят в целых количествах. В Америке это центы. (В некоторых случаях я думаю, что это могут быть мельницы , но пока игнорируйте это.)
Поэтому, когда вы говорите 1,23 доллара, это действительно 123 цента. Всегда, всегда, всегда делайте математику в этих терминах, и у вас все будет хорошо. Для получения дополнительной информации см .:
Отвечая на вопрос напрямую, языки программирования должны просто включать тип Money в качестве разумного примитива.
Обновить
Хорошо, я должен был сказать «всегда» только два раза, а не три раза. Деньги действительно всегда инт; те, кто думает иначе, могут попробовать отправить мне 0,3 цента и показать результат в выписке по счету. Но, как отмечают комментаторы, есть редкие исключения, когда вам нужно выполнять вычисления с плавающей запятой для чисел типа денег. Например, определенные виды цен или процентных расчетов. Даже тогда к ним следует относиться как к исключениям. Деньги приходят и уходят как целые числа, поэтому, чем ближе ваша система к этому, тем разумнее она будет.
источник
Decimal
это единственная разумная система для решения этой проблемы, и ваш комментарий «игнорируйте это сейчас» является предвестником гибели для программистов во всем мире: PПоддержка типа Decimal помогает во многих случаях. Многие языки имеют десятичный тип, но они используются недостаточно.
Понимание приближения, которое происходит при работе с представлением действительных чисел, важно. Использование как десятичных, так и типов с плавающей точкой
9 * (1/9) != 1
является правильным утверждением. Когда константы оптимизатор может оптимизировать расчет так, чтобы он был правильным.Предоставление приблизительного оператора поможет. Однако такие сравнения проблематичны. Обратите внимание, что 0,9999 триллиона долларов примерно равны 1 триллиону долларов. Не могли бы вы внести разницу в моем банковском счете?
источник
0.9999...
триллион долларов точно равен 1 триллиону долларов на самом деле.0.99999...
. Все они усекаются в какой-то момент, что приводит к неравенству.0.9999
достаточно ровно для техники. Для финансовых целей это не так.Нам сказали, что делать на первом курсе (на втором курсе) по информатике, когда я поступил в университет (этот курс также был обязательным условием для большинства научных курсов)
Я вспоминаю лектора, который говорил: «Числа с плавающей точкой являются приблизительными значениями. Используйте целочисленные типы для денег. Используйте FORTRAN или другой язык с числами BCD для точных вычислений». (а затем он указал на приближение, используя тот классический пример 0,2, который невозможно точно представить в двоичной плавающей запятой). Это также оказалось на той неделе в лабораторных упражнениях.
Та же лекция: «Если вам нужно получить больше точности от чисел с плавающей запятой, рассортируйте свои термины. Добавляйте вместе небольшие числа, а не большие». Это застряло в моей голове.
Несколько лет назад у меня была сферическая геометрия, которая должна была быть очень точной и при этом быстрой. 80-битное двойное на ПК не сокращало его, поэтому я добавил в программу несколько типов, которые сортировали термины перед выполнением коммутативных операций. Проблема решена.
Прежде чем жаловаться на качество гитары, научитесь играть.
Четыре года назад у меня был сотрудник, который работал в JPL. Он выразил недоверие, что мы использовали Фортран для некоторых вещей. (Нам нужно было очень точное численное моделирование, рассчитанное в автономном режиме.) «Мы заменили весь этот FORTRAN на C ++», - сказал он с гордостью. Я перестал удивляться, почему они пропустили планету.
источник
1.0 + 0.1 + ... + 0.1
(повторяется 10 раз) возвращается по1.0
мере округления каждого промежуточного результата. Делая это наоборот, вы получаете промежуточные результаты0.2
,0.3
...,1.0
и , наконец2.0
. Это крайний пример, но с реалистичными числами с плавающей запятой подобные проблемы случаются. Основная идея заключается в том, что добавление одинаковых по размеру чисел приводит к наименьшей ошибке. Начните с наименьших чисел, поскольку их сумма больше и, следовательно, лучше подходит для добавления к более крупным.Я не верю, что что-то может или должно быть сделано на уровне языка.
источник
Decimal
в тестах на равенство. Разница между1.0m/7.0m*7.0m
и1.0m
может быть на много порядков меньше разницы1.0/7.0*7.0
, но она не равна нулю.По умолчанию языки должны использовать рациональные числа с произвольной точностью для нецелых чисел.
Те, кому нужно оптимизировать, всегда могут попросить поплавки. Использование их по умолчанию имело смысл в C и других языках системного программирования, но не в большинстве популярных сегодня языков.
источник
double
. Если расчет должен быть точным с точностью до доли на миллион, лучше потратить микросекунду на его вычисление с точностью до нескольких частей на миллиард, чем на абсолютно точную секунду.Две большие проблемы, связанные с числами с плавающей запятой:
Первый тип ошибки может быть исправлен только путем предоставления составного типа, который включает в себя значение и информацию о единицах. Например,
length
илиarea
значение, которое включает единицу (метры или квадратные метры или футы и квадратные футы соответственно). В противном случае вы должны усердно работать с одним типом единиц измерения и переходить на другой, только когда мы передаем ответ человеку.Второй тип неудачи - это концептуальная неудача. Неудачи проявляются, когда люди думают о них как об абсолютных числах. Это влияет на операции равенства, кумулятивные ошибки округления и т. Д. Например, может быть правильным, что для одной системы два измерения эквивалентны в пределах определенного диапазона погрешности. Т.е. .999 и 1.001 - это примерно то же самое, что и 1.0, когда вас не волнуют различия, которые меньше +/- .1. Однако не все системы так снисходительны.
Если есть необходимость в каком-либо языковом уровне, я бы назвал это точностью равенства . В NUnit, JUnit и аналогично построенных средах тестирования вы можете контролировать точность, которая считается правильной. Например:
Если, например, C # или Java были изменены для включения оператора точности, это может выглядеть примерно так:
Однако, если вы предоставляете такую функцию, вы также должны рассмотреть случай, когда равенство хорошо, если стороны +/- не совпадают. Например, + 1 / -10 будет считать два числа эквивалентными, если одно из них будет в пределах 1 больше или на 10 меньше первого числа. Для обработки этого случая вам может понадобиться добавить
range
ключевое слово:источник
Что могут делать языки программирования? Не знаю, есть ли один ответ на этот вопрос, потому что все, что компилятор / интерпретатор делает от имени программиста, чтобы облегчить его жизнь, обычно работает против производительности, ясности и читабельности. Я думаю, что и способ C ++ (платите только за то, что вам нужно), и способ Perl (принцип наименьшего удивления) допустимы, но это зависит от приложения.
Программистам все еще нужно работать с языком и понимать, как он обрабатывает числа с плавающей запятой, потому что если они этого не сделают, они сделают предположения, и однажды предписанное поведение не будет соответствовать их предположениям.
Мой взгляд на то, что программист должен знать:
источник
Используйте разумные значения по умолчанию, например, встроенную поддержку для декали.
Groovy делает это довольно хорошо, хотя, приложив немного усилий, вы все равно можете написать код, чтобы ввести неточность с плавающей запятой.
источник
Я согласен, что на уровне языка делать нечего. Программисты должны понимать, что компьютеры являются дискретными и ограниченными, и что многие из представленных в них математических понятий являются лишь приблизительными.
Не берите в голову с плавающей точкой. Нужно понимать, что половина битовых комбинаций используется для отрицательных чисел и что 2 ^ 64 на самом деле довольно мала, чтобы избежать типичных проблем с целочисленной арифметикой.
источник
x
==y
не означает, что выполнение вычисленияx
даст тот же результат, что и выполнение того же вычисленияy
).Языки могут сделать одно - убрать сравнение на равенство из типов с плавающей запятой, кроме прямого сравнения со значениями NAN.
Проверка на равенство будет существовать только в виде вызова функции, которая принимает два значения и дельту, или для языков, подобных C #, которые позволяют типам иметь методы EqualsTo, который принимает другое значение и дельту.
источник
Мне кажется странным, что никто не указал на рациональную числовую уловку семьи Лисп.
Серьезно, откройте sbcl и сделайте следующее:
(+ 1 3)
и вы получите 4. Если*( 3 2)
вы получите 6. Теперь попробуйте,(/ 5 3)
и вы получите 5/3 или 5 третей.Это должно помочь в некоторых ситуациях, не так ли?
источник
Одна вещь , которую я хотел бы видеть , было бы признание того, что
double
кfloat
следует рассматривать как расширяющийся преобразования, в то время какfloat
вdouble
сужении (*). Это может показаться нелогичным, но подумайте, что на самом деле означают типы:Если кто-то
double
имеет наилучшее представление величины «одна десятая» и преобразует ее вfloat
, результат будет «13 421 773,5 / 134 217 728, плюс или минус 1/268 435 456 или около того», что является правильным описанием значения.Напротив, если кто-то
float
имеет наилучшее представление величины «одна десятая» и преобразует ееdouble
в результат, то получится «13 421 773,5 / 134 217 728 плюс или минус 1/72 057 594 037 927 936 или около того» - уровень подразумеваемой точности что неверно более чем в 53 миллиона раз.Хотя стандарт IEEE-744 требует, чтобы математика с плавающей запятой выполнялась так, как если бы каждое число с плавающей запятой представляло точную числовую величину точно в центре его диапазона, это не должно подразумевать, что значения с плавающей запятой фактически представляют эти точные значения. числовые величины. Скорее, требование, чтобы значения считались в центре их диапазонов, вытекает из трех фактов: (1) вычисления должны выполняться так, как будто операнды имеют некоторые конкретные точные значения; (2) согласованные и документированные предположения более полезны, чем противоречивые или недокументированные; (3) если кто-то собирается сделать последовательное допущение, никакое другое непротиворечивое допущение не может быть лучше, чем допущение, что величина представляет центр его диапазона.
Кстати, я помню, примерно 25 лет назад кто-то придумал числовой пакет для C, который использовал «типы диапазонов», каждый из которых состоял из пары 128-битных чисел с плавающей запятой; Все расчеты будут выполняться таким образом, чтобы вычислить минимальное и максимальное возможное значение для каждого результата. Если выполнить большой итеративный расчет и получить значение [12.53401391134 12.53902812673], можно быть уверенным, что хотя многие цифры точности были потеряны из-за ошибок округления, результат все равно можно было бы разумно выразить как 12,54 (и это не было т действительно 12,9 или 53,2). Я удивлен, что не видел никакой поддержки таких типов ни в одном из основных языков, тем более что они выглядели бы подходящими для математических модулей, которые могут работать с несколькими значениями параллельно.
(*) На практике часто бывает полезно использовать значения двойной точности для хранения промежуточных вычислений при работе с числами одинарной точности, поэтому необходимость использования преобразования типов для всех таких операций может раздражать. Языки могли бы помочь, имея тип "нечеткого двойного", который выполнял бы вычисления как двойные, и мог бы свободно приводиться к и от одиночного; это было бы особенно полезно, если бы функции, которые принимают параметры типа
double
и возврата,double
могли быть помечены так, чтобы они автоматически генерировали перегрузку, которая вместо этого принимает и возвращает «нечеткий двойной».источник
Если бы больше языков программирования брали страницу из баз данных и позволяли разработчикам указывать длину и точность своих числовых типов данных, они могли бы существенно снизить вероятность ошибок, связанных с плавающей точкой. Если бы язык позволил разработчику объявить переменную как Float (2), указывая, что им нужно число с плавающей запятой с двумя десятичными цифрами точности, он мог бы выполнять математические операции намного безопаснее. Если бы он сделал это, представив переменную как внутреннее целое число и разделив на 100, прежде чем выставить значение, он мог бы улучшить скорость, используя более быстрые целочисленные арифметические пути. Семантика Float (2) также позволит разработчикам избежать постоянной необходимости округлять данные перед их выводом, поскольку Float (2) по своей природе округляет данные до двух десятичных точек.
Конечно, вам нужно разрешить разработчику запрашивать максимальное значение с плавающей запятой, когда разработчику нужна такая точность. И вы могли бы представить проблемы, когда слегка отличающиеся выражения одной и той же математической операции дают потенциально разные результаты из-за промежуточных операций округления, когда разработчики не имеют достаточной точности в своих переменных. Но, по крайней мере, в мире баз данных, это не так уж важно. Большинство людей не занимаются научными вычислениями, которые требуют большой точности в промежуточных результатах.
источник
Float(2)
Как вы предложить не следует называтьFloat
, так как нет ничего плавающим здесь, конечно , не «запятой».Это выше применимо в некоторых случаях, но на самом деле не является общим решением для работы со значениями с плавающей запятой. Реальное решение состоит в том, чтобы понять проблему и научиться решать ее. Если вы используете вычисления с плавающей точкой, вы всегда должны проверять, являются ли ваши алгоритмы численно устойчивыми . Существует огромная область математики / информатики, которая связана с этой проблемой. Это называется числовой анализ .
источник
Как отмечалось в других ответах, единственный реальный способ избежать ловушек с плавающей запятой в финансовом программном обеспечении - не использовать его там. Это действительно может быть осуществимо - если вы предоставите хорошо спроектированную библиотеку, посвященную финансовой математике .
Функции, предназначенные для импорта оценок с плавающей точкой, должны быть четко обозначены как таковые и снабжены параметрами, соответствующими этой операции, например:
Единственный реальный способ избежать ловушек с плавающей запятой в целом - это образование - программисты должны читать и понимать что-то вроде того, что должен знать каждый программист об арифметике с плавающей запятой .
Несколько вещей, которые могут помочь, хотя:
isNear()
функцию.источник
Большинство программистов были бы удивлены тем, что COBOL понял это правильно ... в первой версии COBOL не было плавающей запятой, только десятичная, и традиция в COBOL продолжалась до сегодняшнего дня, когда первое, о чем вы думаете, когда объявляете число, является десятичным. .. с плавающей точкой будет использоваться только если вам действительно это нужно. Когда появился Си, по какой-то причине не было примитивного десятичного типа, так что, на мой взгляд, именно здесь начались все проблемы.
источник