Блин, сложная же тема эти числа с плавающей точкой. По крайней мере, для меня.
Вот есть binary floating point numbers, т.е. числа с плавающей точкой, у которых base == 2.
Все мы прекрасно знаем, какие проблемы они могут сулить в руках неопытного разработчика:
— Т.к. они обладают ограниченной точностью, при выполнении арифметических операций может постепенно накапливаться ошибка из-за выполняемых округлений
— Некоторые числа не могут быть представлены вовсе (например, какие-нибудь 0.1, как описано тут, или 1/3)
— Как результат предыдущих пунктов, сравнение нельзя производить напрямую через operator==, его надо осуществлять при помощи epsilon
— ???
Есть такая штука, как decimal floating point numbers.
Они, согласно вики, позволяют избегать ошибок округления, связанных с конвертацией между десятичным и двоичным представлением, т.е. какие-нибудь 0.1 там уже представить можно.
Чего там сделать нельзя, так это, насколько я понимаю, избежать ситуации с числами наподобие 1/3 и всё теми же же ошибками округления, связанными с ограниченным precision'ом (да, он хоть и больше, чем у binary floating point numbers, но всё же имеется).
Есть ещё fixed point numbers, которые, если говорить грубо, просто состоят из двух целочисленных значений -- одно для целой части, а другое для дробной, причём дробная часть имеет фиксированное кол-во знаков после запятой.
В общем, суть такова.
Имеется:
— Приложение, написанное на C++ и использующее фреймворк, из которого ко мне в код попадают переменные типа double
— Доступа к исходному коду данного фреймворка у меня нет, изменять я его не могу
— В приложении выполняются разнообразные операции над числами с плавающей точкой, после чего они возвращаются обратно во фреймворк
Хочется:
— Минимизировать кол-во ошибок, которые можно допустить при работе с дробными числами в данном случае
Кол-во значащих цифр после запятой мне известно, ожидаемый фреймворком алгоритм округления в случае превышения precision'а тоже.
Что в таком случае посоветуете сделать?
— Продолжать использовать double'ы, не забывая о корректном сравнении и прочих подводных камнях
— Начать использовать decimal floating point numbers и получить преимущество (?). Сравнивать их через operator== без epsilon можно, кстати?
— Начать использовать fixed point numbers и получить преимущество (?)
— Переводить double'ы в int'ы, выполнять над ними все необходимые операции и конвертировать обратно в double для передачи фреймворку?
— ???
Любые комментарии и советы приветствуются, буду признателен за ваши ответы.
Здравствуйте, FrozenHeart, Вы писали:
FH>- В приложении выполняются разнообразные операции над числами с плавающей точкой, после чего они возвращаются обратно во фреймворк FH>Хочется: FH>- Минимизировать кол-во ошибок, которые можно допустить при работе с дробными числами в данном случае
Наверное, самого главного не написал — что это за самые "разнообразные операции"? От их вида и будет зависеть какое из решений окажется лучше. Без этого остальные рассуждения будут носить слегка абстрактный характер :)
FH>- Продолжать использовать double'ы, не забывая о корректном сравнении и прочих подводных камнях
Если точность устраивает, то отличный вариант. Быстрый и простой. Конечно, получаемая точность будет зависеть и от последовательности "разнообразных операций" и от самого распределения входных данных. Не зная этого ничего верный ответ не дать. Нужно самому в конкретной задаче оценить погрешности. При этом также не стоит забывать, что иногда погрешности можно значительно нивелировать незначительными изменениями алгоритма (вроде применения суммирования Кэхэна) даже оставаясь в рамках типа double.
FH>- Начать использовать decimal floating point numbers и получить преимущество (?).
Ну это явный аутсайдер.
Как верно заметил, decimal floating point позволяет избежать сложностей при округлении при конвертации между десятичным и двоичным представлениями. Если на входе и выходе у тебя уже и так тип double зафиксирован, то это единственно преимущество тут же уничтожается.
Выгода будет только в очень редких случаях, когда, например, пол-алгоритма будет занимать операция деления на число 5. Тогда ради её упрощения и можно к десятичному основанию перейти. FH> Сравнивать их через operator== без epsilon можно, кстати?
Конечно можно. Ровно с таким же смыслом, как и обычные двоичные floating point числа :)
FH>- Начать использовать fixed point numbers и получить преимущество (?)
Опять же, зависит от операций и самих чисел. Для fixed-point, например, очень тяжело даётся операция ((x / basek) * basek) , которая вообще не представляет проблем для floating point (base=2 для binary floating, base=10 для decimal, k — натуральное). Есть среди твоих операций операция деления? На какие числа? Вот из этих соображений и решать вопрос о применимости fixed-point.
FH>- Переводить double'ы в int'ы, выполнять над ними все необходимые операции и конвертировать обратно в double для передачи фреймворку?
А на низком уровне double что-ли не так процессорами и эмуляторами обрабатываются? От того, что тоже самое будет сделано будет вручную, точность магически не появится.
Вот заменить double на quadruple для промежуточных расчётов — уже получше вариант (опять же не важно, как это будет реализовано — аппаратно или вручную "через int'ы"). Так по крайней мере можно легко часть погрешностей перенести в младшие биты длинного числа, которые потом не помешают при возврате к double. Ну или помешают и нужно будет octuple брать :)
Ну и конечно, не самому это писать, а библиотеку вроде MPFR использовать. И не забывать, что даже поставив огромную длину и точности для чисел, всё равно можно придумать последовательность действий, для которых её не будет достаточно.
Здравствуйте, FrozenHeart, Вы писали:
FH>Есть такая штука, как decimal floating point numbers. FH>Они, согласно вики, позволяют избегать ошибок округления, связанных с конвертацией между десятичным и двоичным представлением, т.е. какие-нибудь 0.1 там уже представить можно. FH>Чего там сделать нельзя, так это, насколько я понимаю, избежать ситуации с числами наподобие 1/3 и всё теми же же ошибками округления, связанными с ограниченным precision'ом (да, он хоть и больше, чем у binary floating point numbers, но всё же имеется).
Им присущи все вышеперечисленные для binary floating point numbers недостатки.
— Они обладают ограниченной точностью, при выполнении арифметических операций может постепенно накапливаться ошибка из-за выполняемых округлений
— Некоторые числа не могут быть представлены вовсе (например, какие-нибудь 0.3(3), как описано выше
— Как результат предыдущих пунктов, сравнение нельзя производить напрямую через operator==, его надо осуществлять при помощи epsilon
Преимущество тоже описал — "позволяют избегать ошибок округления, связанных с конвертацией между десятичным и двоичным представлением". Но про недостаток — меньшая скорость обработки, забыл.
FH>В общем, суть такова.
FH>Имеется: FH>- Приложение, написанное на C++ и использующее фреймворк, из которого ко мне в код попадают переменные типа double FH>Любые комментарии и советы приветствуются, буду признателен за ваши ответы.
Без знания области применения сложно что-то посоветовать.
Но если работа с тем фреймворком составляет существенную часть логики программы, вероятно использование double очень неплохой вариант, или лучший, или единственно правильный. Но это как раз от назначения программы и характера расчетов зависит.
W>Наверное, самого главного не написал — что это за самые "разнообразные операции"?
Не написал конкретику из-за того, что в общем случае вычисления могут быть абсолютно любыми -- грубо говоря, программа выполняет расчёты согласно формулам, занесённым пользователем в конфигурационный файл. Т.е. это может быть и сложение, и вычитание, и умножение, и деление.
W>Конечно можно. Ровно с таким же смыслом, как и обычные двоичные floating point числа
Я думал, что из-за того, что здесь любое "finite" число представляется однозначно, без "искажений", это вполне возможно.
Ну, т.е. если мы используем binary floating point numbers, то, понятное дело, следующий код -- это фейл:
if (number == 0.1) { /* do smth */ }
Однако в случае decimal'ов все "finite" числа представляются точно, без "искажений" и округлений, разве нет?
W>Есть среди твоих операций операция деления? На какие числа? Вот из этих соображений и решать вопрос о применимости fixed-point.
Ответил ранее
W>А на низком уровне double что-ли не так процессорами и эмуляторами обрабатываются? От того, что тоже самое будет сделано будет вручную, точность магически не появится.
Почему же? Взять, например, всё то же число 0.1.
Если мы оперируем им в случае binary floating point numbers, то следующий цикл запросто накопит существенную ошибку:
for (int i = 0; i < 10000000; ++i)
{
number += 0.1;
}
Однако если бы мы сразу перевели все дробные значения в int'ы, то ошибка была бы только в моменты конвертации, а сложение происходило бы без проблем:
int int_number = static_cast<int>(number * 100); // Допустим, нам важен precision == 2for (int i = 0; i < 10000000; ++i)
{
int_numbernumber += static_cast<int>(0.1 * 100);
}
P>Преимущество тоже описал — "позволяют избегать ошибок округления, связанных с конвертацией между десятичным и двоичным представлением". Но про недостаток — меньшая скорость обработки, забыл.
Вы точно про binary floating point numbers говорите? Может, имелись ввиду decimal'ы? Цитата вроде неправильная.
P>Без знания области применения сложно что-то посоветовать.
Область применения -- финансовая сфера (да, и при этом фреймворк подсовывает double для хранения цен).
Операции могут быть самими разнообразными, ответил рядом -- грубо говоря, программа выполняет расчёты согласно формулам, занесённым пользователем в конфигурационный файл. Т.е. это может быть и сложение, и вычитание, и умножение, и деление.
P>Но если работа с тем фреймворком составляет существенную часть логики программы, вероятно использование double очень неплохой вариант, или лучший, или единственно правильный.
Да, операции над ценами выполняются довольно часто.
FH>Ну, т.е. если мы используем binary floating point numbers, то, понятное дело, следующий код -- это фейл:
FH>
FH>if (number == 0.1) { /* do smth */ }
FH>
FH>Однако в случае decimal'ов все "finite" числа представляются точно, без "искажений" и округлений, разве нет?
Блин, сказал, как идиот.
Если number в приведённом примере изначально присваивается 0.1 и над ним не производится никаких арифметических операций, то сравнение number == 0.1 и в случае binary floating point numbers сработает отлично.
Скорее, пример должен быть такой:
double number = 0.1 + 0.1 + 0.1;
if (numbers == 0.3) { /* do smth */ }
Вот такие ситуации в случае decimal floating point numbers разве могут когда-то зафейлиться?
Здравствуйте, FrozenHeart, Вы писали:
FH>- Приложение, написанное на C++ и использующее фреймворк, из которого ко мне в код попадают переменные типа double FH>- Доступа к исходному коду данного фреймворка у меня нет, изменять я его не могу
Что известно про числа и задачу? Матфизика, финансы, что-то другое?
FH>- В приложении выполняются разнообразные операции над числами с плавающей точкой, после чего они возвращаются обратно во фреймворк FH>Хочется: FH>- Минимизировать кол-во ошибок, которые можно допустить при работе с дробными числами в данном случае FH>Кол-во значащих цифр после запятой мне известно, ожидаемый фреймворком алгоритм округления в случае превышения precision'а тоже. FH>Что в таком случае посоветуете сделать? FH>- Продолжать использовать double'ы, не забывая о корректном сравнении и прочих подводных камнях
В общем случае — да, остаться на привычных binary floats.
Причина очень проста: base=10 ничего положительного тебе не даст (у тебя уже на входе и выходе binary), и они хуже потому, что с ростом base увеличиваются ошибки округления и дребезг значения младшего разряда.
Decimal floats имеют смысл только в тех случаях, когда числа гарантированно представляются как десятичная дробь (грубо говоря, это финансы), а где не представляются — сразу округляются после вычисления до нужной точности.
FH>- Начать использовать decimal floating point numbers и получить преимущество (?). Сравнивать их через operator== без epsilon можно, кстати?
Преимущества нет. Сравнивать тоже нельзя, кроме случаев, когда расчёты гарантированно ограничены стандартными финансовыми операциями, типа 4 арифметики и проценты.
FH>- Начать использовать fixed point numbers и получить преимущество (?)
Если это финансы, да, так лучше. Иначе — см. выше.
Здравствуйте, FrozenHeart, Вы писали:
W>>Конечно можно. Ровно с таким же смыслом, как и обычные двоичные floating point числа FH>Я думал, что из-за того, что здесь любое "finite" число представляется однозначно, без "искажений", это вполне возможно.
Точно также 1/3 в десятичке не будет точным. И, как уже сказал, дребезг младшего разряда и погрешность округления растёт с ростом base.
FH>Однако в случае decimal'ов все "finite" числа представляются точно, без "искажений" и округлений, разве нет?
Здравствуйте, FrozenHeart, Вы писали:
FH>Вы точно про binary floating point numbers говорите? Может, имелись ввиду decimal'ы? Цитата вроде неправильная.
Сравниваю binary floating point numbers и decimal floating point numbers. И не вижу между ними особой разницы. Но нюансы есть.
FH>Область применения -- финансовая сфера (да, и при этом фреймворк подсовывает double для хранения цен).
Фреймворк изначально предназначен для финасовых расчетов?
FH>Операции могут быть самими разнообразными, ответил рядом -- грубо говоря, программа выполняет расчёты согласно формулам, занесённым пользователем в конфигурационный файл. Т.е. это может быть и сложение, и вычитание, и умножение, и деление.
Но только финасовые?
В фреймворк что передается, эти формулы и числовые данные, а возвращается результат расчета? Что с ним нужно сделать — показать пользователю, записать в БД или отправить в следущий по цепочке расчет.
P>>Но если работа с тем фреймворком составляет существенную часть логики программы, вероятно использование double очень неплохой вариант, или лучший, или единственно правильный. FH>Да, операции над ценами выполняются довольно часто.
Почему имеено над ценами?
Результат расчетов это денежные суммы с точностью до целых копеек(центов) или что-то другое?
N>Нет. Только конечные десятичные дроби. Ваш кэп.
Но ведь неконечные хоть в каком виде представь -- они всё равно не могут быть отражены точно (тот же случай с 1/3).
FH>>Область применения -- финансовая сфера (да, и при этом фреймворк подсовывает double для хранения цен). P>Фреймворк изначально предназначен для финасовых расчетов?
Фреймворк передаёт в мою программу информацию о проведённых операциях. Помимо всей остальной информации, там имеются и цены. Мне необходимо провести над ними некоторые манипуляции в виде арифметических операций и отдать обратно фреймворку, который уже засунет результат в кучу разных мест -- свою внутреннюю БД, в терминал клиентов и т.д.
P>Результат расчетов это денежные суммы с точностью до целых копеек(центов)
Да.
Здравствуйте, FrozenHeart, Вы писали:
FH>Фреймворк передаёт в мою программу информацию о проведённых операциях. Помимо всей остальной информации, там имеются и цены. Мне необходимо провести над ними некоторые манипуляции в виде арифметических операций и отдать обратно фреймворку, который уже засунет результат в кучу разных мест -- свою внутреннюю БД, в терминал клиентов и т.д.
Работай с double.
Если твои вычисления настолько сложны и многоэтапны, что могут воникнуть погрешности связанные с вычислениями, а не представлением чисел, можно делать округление перед возвратом значений в фреймворк. Возможно округление нужно и для каких-то промежуточных результатов, но это зависит от задачи. Обрати внимание на ситуации, когда сумма каких-то показателей полученных расчетным путём должна быть равна какой-то фиксированной сумме, возможно придется где-то накидывать/убирать копейку, это распространенная проблема, но со способом представления чисел она тоже не связана.
P>Работай с double. P>Если твои вычисления настолько сложны и многоэтапны, что могут воникнуть погрешности связанные с вычислениями, а не представлением чисел, можно делать округление перед возвратом значений в фреймворк. Возможно округление нужно и для каких-то промежуточных результатов, но это зависит от задачи. Обрати внимание на ситуации, когда сумма каких-то показателей полученных расчетным путём должна быть равна какой-то фиксированной сумме, возможно придется где-то накидывать/убирать копейку, это распространенная проблема, но со способом представления чисел она тоже не связана.
Почему бы тогда уж fixed point numbers не юзать?
Здравствуйте, FrozenHeart, Вы писали:
FH>Почему бы тогда уж fixed point numbers не юзать?
С фреймворков же обмениваешься в double, никакого смысла переводить из одного в другое и обратно нет. А так да, если бы программа не завесила от фреймворка, вариант с fixed point numbers возможно и был бы разумным. Но в любом же случае от проблем с округлением он не спасает. И в финансовых расчетах ситуации наподобие 1/3 сплошь и рядом, и результат все равно будет зависить от того сколько знаков после точки зафиксировать в "fixed point numbers" и когда округлять.
Здравствуйте, FrozenHeart, Вы писали:
N>>Нет. Только конечные десятичные дроби. Ваш кэп. FH>Но ведь неконечные хоть в каком виде представь -- они всё равно не могут быть отражены точно (тот же случай с 1/3).
Они могут быть отражены как 1/3 Но в общем да, вопрос не в этом. Вопрос в следующем. Представь себе, что у тебя десятичная плавучка с 6 цифрами мантиccы. Если ты вычисляешь 9999.99+0.01, получаешь 10000.0, и этот результат точный. Если 9999.99+0.02, получаешь 10000.0, и это уже результат неточный (7-я цифра не влезла). IEEE754 предоставляет возможность узнавать про такие неточные результаты через inexact condition (флаг или исключение), и если ты считаешь в десятичной, эта ошибка будет генерироваться только если действительно происходит потеря точности в результате выхода нужных тебе цифр за пределы представляемого (как в этом примере потеряли один цент), а если в двоичном, она будет происходить практически при любых вычислениях с дробными частями, и ты не опознаешь реальный факт такой потери точности на фоне такого шума.
Вот это то единственное, ради чего decimal float вообще могут быть нужны (а если инфраструктура такого не предоставляет, то она в целом бессмысленна, и лучше работать в более распространённых двоичных числах, или в fixed).
Здравствуйте, pagid, Вы писали:
P>Здравствуйте, FrozenHeart, Вы писали:
FH>>Почему бы тогда уж fixed point numbers не юзать? P>С фреймворков же обмениваешься в double, никакого смысла переводить из одного в другое и обратно нет. А так да, если бы программа не завесила от фреймворка, вариант с fixed point numbers возможно и был бы разумным. Но в любом же случае от проблем с округлением он не спасает. И в финансовых расчетах ситуации наподобие 1/3 сплошь и рядом, и результат все равно будет зависить от того сколько знаков после точки зафиксировать в "fixed point numbers" и когда округлять.
Но в fixed это (как разделить на 3 и т.п.) контролируется явно, чем и лучше.
Здравствуйте, pagid, Вы писали:
P>Здравствуйте, netch80, Вы писали:
N>>Но в fixed это (как разделить на 3 и т.п.) контролируется явно, чем и лучше. P>Как оно контролируется?
Вручную если считается в целых типа 1/100 копейки, то в этих целых и поддерживается (например, чтобы 100 разрезалось на 33+33+34).
Здравствуйте, netch80, Вы писали:
N>Вручную если считается в целых типа 1/100 копейки,
Какой смысл считать в 1/100 копейки, а не в копейках?
N> то в этих целых и поддерживается (например, чтобы 100 разрезалось на 33+33+34).
Верно, уже упоминал об этом выше. Но тоже самое придется делать и при представлении чисел другими способами.
Но, например, нужно расчать что-то с участием в выражении не только денег, а каких-нибудь процентов или коээфициентов, и точность их представления нужна не 0.01 или даже не 0.0001. И получится выражение с разными типами чисел, их приведением возможно в одном выражение и вероятными проблемами. С double этой проблемы не будет.
Вовсе не призываю использовать double в финансовых расчетах, боже упаси Просто к тому, что отказ от "неточного" представления чисел вовсе не решает всех проблем и не отменяет необходимости думать.