|
|
|||||||||||||||||||||||||||||
|
Загадки округленияИсточник: delphikingdom
Автор: Алексей Михайличенко, Королевство DelphiЭта статья задумывалась как краткий ответ на вопрос N 40789, где обсуждались ошибки в функциях округления. Как сказал автор, "мне все равно что "бухгалтерское", что "арифметическое" - главное чтоб считало также как в Excel'е". Другими словами, возникла необходимость прояснить, чем отличается бухгалтерское округление от арифметического, какое из них реализовано в Excel'e - этом "гении чистой красоты", и почему некоторые Delphi-функции странным образом работают иначе. Стоило копнуть эту тему поглубже, и новости повалили как из сказочного горшочка с кашей. Не претендуя на академическое раскрытие темы, поделюсь материалом, имеющимся на данный момент. Предлагаю следующий план:
Итак, все мы в свое время обнаруживали, (а если нет - то вас еще ждет это волнующее открытие), что 0.1 в компьютере не равно 0.1. Читали статью "Неочевидные особенности вещественных чисел" Антона Григорьева, ужасались результатам сравнения и вычитания extended, погружались в дебри внутреннего представления чисел с плавающей запятой и размышляли о бесконечных цифровых хвостах. Интересующимся этой кухней порекомендуем классику - Дональд Кнут: "Искусство программирования", том 2, Глава 4. "Арифметика", где увлекательнейше описана история позиционных систем счисления, и в общем виде рассмотрены алгоритмы арифметики с плавающей запятой. Удачно дополняет ее статья Дэвида Голдберга "Что должен знать каждый ученый-компьютерщик о об арифметике с плавающей запятой". В ней приводятся некоторые теоремы, позволяющие оценить величину ошибок, возникающих в машинной арифметике, рассматриваются соответствующие IEEE-стандарты, и вопросы их реализации. Возвращаясь из этих научных миров в реальную жизнь, приходится сделать неутешительный вывод: по сравнению со школьным курсом арифметики компьютеры считают неточно. Эта мысль достаточно тяжело укладывается в голове. Действительно, с первых уроков математики нам показывали рулетки, линейки, штангенциркули и микрометры, и говорили о том, что все измерения имеют погрешность. Демонстрировали картинки с оптическими обманами и говорили о несовершенстве органов чувств. И в противовес этому превозносили саму математику, ее арифметические действия, как символ незыблемой правильности. Ведь еще Пифагор понял, что числа существуют независимо от нашего сознания, и логические рассуждения над ними переживут века. В народе это отразилось в поговорке "как дважды два - четыре" - то есть заведомо верно. Чтобы снять с компьютера незаслуженный ореол научной святости, представим себе, что это всего лишь один из школьных измерительных приборов, со своими ограничениями и некоторым классом точности. А внутри него происходит вот что. Например, пусть нам нужно сложить два числа: 10 и 20. Берется школьная линейка и огурец. Отрезается от огурца два кусочка: 10 и 20 мм длиной, складывается вдоль, и этой же кривоватой линейкой измеряется длина результата. Получаем ответ с некоторой погрешностью. Именно с такой степенью доверия нам придется относиться к результатам компьютерных вычислений с плавающей запятой. Иногда приходится слышать, будто компьютеры считают приближенно. Но это совершенно не так. Для обсуждения этого вопроса срочно определимся с терминами. Приближенные вычисления - давний и почтенный раздел математики. Он дает набор неких практических правил по обработке данных, полученных нашими несовершенными измерительными и вычислительными приборами. Именно следуя этим правилам, наша цивилизация вершила настоящие чудеса - строила храмы и мосты, предсказывала затмения, летала на фанерных аэропланах и керосиновых ракетах и стреляла из пушек за горизонт. И все это - заметьте - совершенно без помощи компьютеров. Что же это за столь замечательные правила? Согласно Правилам Приближенных Вычислений (ППВ), каждому числу, полученному в результате измерений или вычислений, неотрывно сопутствует некая величина, характеризующая его точность. Эта величина может быть выражена абсолютной погрешностью, например (10 +/- 1); относительной погрешностью (10 +/- 10%); количеством значащих цифр, которое обычно выражается в записи (1.000); или записью числа в виде интервала возможных значений (99..101). Эти формы записи в общем-то эквивалентны, и могут быть получены одна из другой. Суть в том, что мы:
Например, при сложении приближенных чисел достаточно просто сложить их абсолютные прогрешности. При умножении и делении - сложить относительные погрешности. Таким образом можно определить погрешность результата любой цепочки арифметических действий. Более подробно о ППВ можно прочитать в любом математическом справочнике. Вернемся к нашим баранам - компьютерам. Что делают они?
Как видим, на алгоритмы ППВ похоже с точностью до наоборот. Что же за странную задачу решает компьютер, и кому она такая нужна? Подобный порядок вычислений имеет смысл лишь тогда, когда контроль точности результата ведется отдельно, либо заложен в саму задачу. В первом случае нам необходимо параллельно с формулами вычислений написать формулы вычисления погрешности. Иллюстрацией второго может быть решение некоего уравнения численными методами, когда берем результат в "артиллерийскую вилку", и итеративно приближаемся к нему с обеих сторон, прекращая вычисления при достижении шагом итерации некоего малого значения. Конечно, для решения учетно-бухгалтерских задач такие подходы выглядят экзотично. Но если вспомнить, что первые компьютеры использовались в основном для решения чисто научных либо военных задач (например, баллистики), то станет понятно, что другого от компьютеров тогда и не требовалось. Ученые той поры привыкли к своенравной вычислительной технике, и учитывали это в соответствующих математических моделях. Представьте, например, баллистическую ракету. Десятиметровая бандура, сделанная из жести. Она не стоит, а скорее висит на захвате стартовой вышки, потому что жесткости у нее никакой - она играет как резиновая. Включаются двигатели, захват отпускают, и представьте, что вы вертикально держите ее на кончике пальца, как карандаш. Карандаш норовит упасть, вы ловите отклонение и пододвигаете палец. А ракета, кроме того, ревет двигателями, норовит сложиться пополам, скрутиться по спирали, да и нужно ее не просто удержать, а запустить именно в заданную сторону. Все это своенравие учитывается в математической модели устойчивости, выворачивается наизнанку и кладется в систему стабилизации и наведения - ящик между гироскопами и двигателями. А перед тем гоняем математическую модель ракеты на аналоговой ЭВМ - наборе операционных усилителей и деталюшек, изготовленных с точностью в лучшем случае 0.1%. И все работает. Ну и скажите, имеет ли значение некоторая погрешность, если пыхтящую аналоговую ЭВМ заменить не совсем точной цифровой? Если исходная математика правильна и обратные связи модели отрабатываю верно, то все будет работать в любом случае. Поэтому не ругайте компьютеры - при правильном подходе из них можно извлечь немалую пользу. Прошли времена железных людей, управляющих железными компьютерами. Теперь мы хотим считать на машинах килограммы, штуки, и конечно же деньги. И чтоб все сходилось до копеечки. А floating-point-вычислители-то остались прежними - пахнущими ракетной копотью и научной романтикой. И если мы хотим получить предсказуемые результаты и нести за них какую-то ответственность, то нам в наших программах придется вернуться к Правилам Приближенных Вычислений, и попытаться организовать их самостоятельно, не надеясь на компьютер, а используя его как вспомогательный инструмент. Как мы уже говорили, ППВ позволяют знать погрешность результата на каждом этапе вычислений. Но для длинных цепочек трехэтажных формул прослеживание погрешности каждого этапа может оказаться делом нелегким, да и необходимо это сравнительно редко - в основном в научных задачах, где погрешность может оказаться соизмеримой с результатом. На практике пользуются упрощенными правилами, основанными на подсчете значащих цифр:
Здесь уместно привести высказывание выдающегося инженера-кораблестроителя академика Крылова, который говорил: "Лишняя вычисленная цифра есть ОШИБКА". Вопреки распространенному мнению, таскание хвостов незначащих цифр вместо их округления вовсе не повышает точности вычислений, а наоборот - может привести к накоплению ошибок в самых неожиданных местах. Ну и кроме этого, речь идет об элементарной грамотности и аккуратности - при выводе результата 10,1230 лишние цифры могут создать впечатление их достоверности, скажем, до четвертого знака, в то время как исходные данные задавались плюс-минус лапоть. А это уже чревато рухнувшими мостами и взорванными реакторами. Недостоверные разряды следует округлять. Поэтому перейдем к фундаментальной операции приближенных вычислений - округлению. Согласно школьного курса математики, при округлении чисел мы отбрасываем ненужные разряды, причем если первая отбрасываемая цифра больше или равна 5, то последняя сохраняемая цифра увеличивается на единицу. Будем называть этот способ "Арифметическим округлением". Недоверчивый читатель, наверное, уже заподозрил, что сейчас пойдет речь и о других способах округления, и тянет руку с вопросом: а зачем, собственно? Чем не устраивает этот, столь родной и знакомый? Проблема заключается в накоплении статистической погрешности при округлении большого количества чисел. Другими словами, многие задачи требуют, чтобы сумма столбика неокругленных значений была равна, или хотя бы близка сумме столбика округленных значений, а арифметическое округление приводит к серьезной и неизбежной ошибке, нарастающей с объемом данных. Причем это вовсе не связано с машинной арифметикой. Чтобы убедиться в этом, выключим компьютер, вооружимся счетами и карандашом, и решим такую задачу. Контора заработала ровно миллион рублей, и поручила бухгалтеру разделить их на тысячу работников пропорционально коэффициенту трудового участия (КТУ). Тот выполнил арифметические действия и получил для каждого работника некоторое число Ni, с некоторым хвостиком дробных копеек. Убедился, что сумма всех Ni дает ровно миллион (мы опускаем проблемы неделимости нацело и бесконечных дробей - пусть хоть сегодня у нас все поделилось). Рассчитал сумму к выдаче - округлил каждое Ni до копеек, и подбил итог. И что же он видит? Итог к выдаче составил что-то вроде один миллион рублей 50 копеек. А где ж эти 50 копеек взять? Он бы уже рад их и из своей получки добавить, да только проверяющие придут, и скажут - батюшки, да у вас бухгалтерия не пляшет - вот вы у нас теперь и попляшете. Плясать бухгалтеру вовсе не хотелось, поэтому вздохнул он и начал разбираться. КТУ в конторе по старой советской традиции ставили с потолка, поэтому он достаточно хорошо подчинялся закону распределения случайных чисел. Соответственно, когда дело доходило до округления, то среди "первых отбрасываемых цифр" было примерно поровну нулей, единичек, двоек и всех остальных цифр. Каждая операция округления вносила свою погрешность (разницу между первоначальным и округленным значением) в зависимости от отброшенного хвостика. При этом погрешности отброшенных единичек (-0.001) компенсировались погрешностями девяток (+0.001), двойки компенсировали восьмерки, и так далее, и лишь погрешности, вносимые при отбрасывании пятерок (+0.005), оставались нескомпенсированными, и накапливались. В среднем на тысяче человек встретилось 100 операций отбрасывания пятерки, каждая из которых дала погрешность пол-копейки. Отсюда и набежали злосчастные 50 копеек. Вот фрагмент расчетной ведомости, демонстрирующий набегание одной копейки при раздаче суммы в 2000 руб.21 коп.:
Если бы бухгалтер был магом и чародеем, он несомненно решил бы проблему так, чтобы какой-нибудь саблезубый тигр откусил руку, или хотя бы нечетное количество пальцев нашему волосатому пращуру, придумавшему десятичную систему счисления, чтобы в ней не осталось "середины". Но он выкрутился хитрее - половину отбрасываемых пятерок стал округлять вверх, а половину - вниз. Чтобы его не обвинили в личных пристрастиях, критерием стала цифра перед пятеркой - если она четная, то округление вниз, иначе вверх. Это правило и называется правилом "Бухгалтерского" (или "Банковского") округления. В нашем примере в строке 5 сумма стала округляться до 100.00, вносимая погрешность стала -0.005, скомпенсировав строку 15, и сумма к выдаче совпала с исходной. Теперь, когда у нас есть больше одного способа округления, возникает извечный вопрос: а какой из них правильный? Любопытно, что в обсуждении этого вопроса спорщики обычно начисто забывают об области применения алгоритма, и вообще каких-либо критериях правильности, а ищут некую правильность в метафизическом, вселенском смысле. Приходилось встречать мнение, что банковское округление характерно для капиталистических стран, а арифметическое - для СССР. Другие доказывали, что арифметическому учат в школе, а банковскому - в ВУЗах. Когда выяснялось, что в некоторых институтах тоже применяют арифметическое, на полном серьезе составляли "черный список" таких ВУЗов и подвергали их осмеянию. Третьи говорили, что это баг от Microsoft, или глюк всех Pentium-ов (или AMD, в зависимости от личных пристрастий). Поэтому хотелось бы знать, существует ли некий общепринятый документ относительно способов округления. И такой документ действительно существует. Это знаменитый стандарт IEEE 754. Любопытна история создания этого документа. В 60-е - 70-е годы, когда компьютеры были большими, каждая линейка компьютеров имела свою программную реализацию вычислений с плавающей запятой, свои форматы представления чисел, точность, представимые диапазоны и правила округления. Соответственно, чудеса, вроде описанных в "Неочевидных особенностях вещественных чисел", были у каждого свои. По воспоминаниям старожилов, на некоторых машинах число могло выглядеть отличным от нуля в операциях сравнения и сложения, но быть чистым нулем при умножении и делении. Чтобы без страха поделить на такое число, его следовало умножить на 1.0 и лишь потом сравнить с нулем. А другие машины могли выдать ошибку переполнения при умножении на 1.0 вполне нормального числа. Были такие малюсенькие числа (но не нули), которые давали переполнение при делении на самих себя. В программах были обычными шаманские вставки вроде X = (X + X) - X. Соответственно, одна и та же программа, даже написанная на стандартном FORTRAN'е, могла давать разные результаты на разных машинах. Для решения этой проблемы в середине 70-х под эгидой IEEE неторопливо начал работу комитет по выработке стандарта 754 - о реализациях вычислений с плавающей запятой. Примерно в это же время Intel начал разработку арифметического сопроцессора i8087 для своих процессоров i8086/88. В качестве консультанта был приглашен профессор Вильям Каган, известный успешным сотрудничеством с Hewlett-Packard. Проект подходил к завершению. В этот сопроцессор удалось втиснуть все лучшее, что было на тот момент. Профессор Каган решил принять участие в работе комитета IEEE, получил от Intel разрешение открыть некоторые спецификации нового сопроцессора (без раскрытия подробностей его реализации), и представил их как проект стандарта. Учитывая, что у конкурентов сопроцессоры были пока лишь в планах, Intel-овские спецификации выгодно отличались продуманностью и завершенностью. Крыть было нечем. Проект де-факто лег в основу стандарта. Текст стандарта по идее можно получить в первоисточнике (http://ieee.org), но обычно ссылаются на сборник связанной с ним информации от IBM (http://www2.hursley.ibm.com/decimal/). Этот стандарт описывает пять способов округления, обязательных для реализации, и два опциональных.
А ведь это далеко не все способы, которые можно вообразить. Спросите, зачем нужно больше? Ответим. Банковское округление - вовсе не панацея для подавления статистической погрешности. Она спасла нашего бухгалтера лишь потому, что округляемые числа были достаточно случайны, то есть появление четных и нечетных цифр перед пятеркой было равновероятно. Но пришли в контору новые времена, и КТУ стали хитро высчитывать на основании затраченного рабочего времени. По нелепому совпадению, для стандартного рабочего месяца это оказалось равно 100005 (как в строке 5). А так как большинство людей работают без прогулов, то значение это стало встречаться очень часто, и итог "К выдаче" вновь оказался больше, чем "Заработано". Для решения этой проблемы известны следующие алгоритмы:
Думаю, при таком обилии алгоритмов вопрос "какой из них единственно верный" ставить как-то неудобно. Правда, IEEE 754 требует, чтобы промежуточные результаты вычислений округлялись по-банковски. Но стандарт этот касается реализаторов сопроцессоров, и призван лишь обеспечить переносимость программ в смысле одинаковости результатов на разных системах, а про бухгалтерию там ничего нет. Поэтому постановщики и разработчики должны сами проработать этот вопрос, и выбрать подходящий алгоритм. Но чем руководствоваться? Нормативные документы редко опускаются до таких "мелочей". Поэтому на практике обычно спонтанно используют арифметическое либо бухгалтерское округление - какое реализовано в языке, а при расчетах с родным государством - считают доли копейки в его пользу, от греха подальше - иначе дороже выйдет, если проверяющие начнут просчитывать контрольные примеры. Чтобы отпустить на обед нашего многострадального бухгалтера, обсудим последний на сегодня аспект деления денег. Для программиста он коварен тем, что внешне выглядит как проблема с округлением - несовпадение итога округленных сумм с исходной. Поэтому бухгалтеры нередко берут расчетную ведомость, и, как сказал классик, "ейною мордою начинают мне в харю тыкать". А проблема не касается ни машинной арифметики, ни алгоритма округления. Сформулировать ее можно так: если трое договорились делить доход поровну, а заработали 10 копеек, то как быть с лишней копейкой? Правильный ответ - решить этот вопрос должна сама бухгалтерия. Вариантов можно предложить три:
На этом позвольте завершить бухгалтерские вопросы. Предположим, что программисты договорились с бухгалтерами о выбранном способе округления, и, сгорая от нетерпения, ринулись к любимым языкам программирования. Что же они нам предлагают?
Думаю, наибольшее внимание читателей привлекла строка со встроенными функциями Delphi. Обсудим их. Прежде всего отметим: на работу функций Delphi сильно влияет установка режима округления процессора командой SetRoundMode. В документации лишь невнятно упомянуто ее влияние на функцию RoundTo, но на самом деле она влияет и на SimpleRoundTo, и вооще практически на все результаты вычислений. В связи с этим категорически не рекомендуется менять режим округления, а при необходимости - делать это лишь кратковременно, и тут же возвращать в значение по умолчанию (rmNearest). Примером нелепого влияния SetRoundMode может служить функция EndOfTheMonth, которая по документации возвращает последний момент текущего месяца, а при SetRoundMode(rmUp) - начинает возвращать первый момент следующего. Приходилось слышать также о проблемах с непонятными ошибками "Invalid floating point operations" внутри ADO, связанные с отличием RoundMode от стандартного. Итак, согласно документации, SimpleRoundTo реализует арифметическое округление, а RoundTo - банковское. Но на самом деле они вытворяют такие чудеса:
Результаты получены на Delphi 7 при режиме округления по умолчанию (rmNearest). Результаты тестирования при других режимах приведены в приложении, но безошибочного поведения согласно какого-либо алгоритма достигнуть так и не удалось. Вообще, результат RoundTo лишь в 50% случаев округления пятерки совпадает с правилом банковского округления. Статистическая погрешность подавляется отвратительно - набегает 13 руб. 45 коп разницы на миллионе записей. Еще меньше похожа ее подружка SimpleRoundTo на обещанное арифметическое округление - менее 40% совпадений, все ошибки в одну сторону (см. результаты тестов в приложении). Возникает вопрос: неужели в Borland не знают об этой ошибке? Оказывается, знают еще с августа 2003 года. Соответствущие Public Reports в Quality Central на Borland Developer Network: 5486, 8070, 8143. Кстати, первый же комментарий под заявкой таков: "Кто-нибудь может объяснить, почему они присвоили этой ошибке такой низкий рейтинг?...". Я не могу. К счастью, нашелся неравнодушный человек по имени John Herbster, который предложил собственную реализацию функций округления, и выложил ее для всеобщего использования. Взять их можно там же, на Borland Developer Network (ссылки под упомянутыми Public Reports, регистрация на BDN бесплатная). В моих тестах они не дали ни одной ошибки, так что всячески рекомендую. Когда обнаружилось, что многие функции округления работают как попало, возникла идея тестирования всех возможных функций округления разных языков. Методика тестирования и результаты тестов - в приложении. Заключение Тема вычислительных алгоритмов - интересная и необъятная. В статье затронуты лишь некоторые аспекты создания надежных, предсказуемых программ. В развитие темы хотелось бы более подробно поговорить о правилах приближенных вычислений (ППВ), рассмотреть их правильное применение в конкретных алгоритмах, в сочетании с округлением в нужных местах. В качестве илюстраций хорошо бы разобрать типичные алгоритмы, например удержания с заработной платы, составление графика выплаты кредита, начисление пени и т.п, показать возникающие ошибки и пути их устранения применением ППВ. Другой аспект проблемы - интеграция ППВ непосредственно в язык программирования. Интересно было бы иметь математический модуль, который прозрачно для программиста вел бы действительно приближенные вычисления, по описанным выше правилам, с точно определяемыми погрешностями. Применение такого средства позволило бы надежно спрятать малоприятные чудеса плавающих запятых, и исключить недоуменные вопросы на Круглом столе. В конце концов, для того и нужен высокоуровневый язык программирования, чтобы скрывать от програмиста особенности реализации сопроцессоров. Одной из попыток в этом направлении является введение типа Currency, который ведет вычисления с автоматическим округлением промежуточных результатов. Недостатком его является жестко заданная точность (видимо, для вычисления квадратных денег). Есть также смутные сведения о не вполне корректных преобразованиях этого типа в процессе вычислений (приведение к float), что чревато ошибками. Так что этот вопрос ждет своих исследователей. И еще. Некоторые из функций содержат "волшебные" вставки вроде x + 0.000001. Нередко эти функции показывали безошибочные результаты, и я как честный человек был вынужден об этом сообщить. Но в глубине души я подозреваю, что такие вставки могут привести к ошибкам на других наборах данных. Так что если все же решите их использовать - будьте осторожны. Не нужно забывать, что мы тестировали только положительные числа. На отрицательных, видимо, такие "хвостики" тоже должны быть с минусом. Для меня также непонятен вопрос, не приведут ли такие вставки к систематическому накоплению ошибки при каких-либо условиях. Так что есть о чем задуматься. Ну и разумеется, хотелось бы выяснить, как обстоят дела с округлением в других продуктах, в частности NET, и других от Microsoft. Если кто-то имеет такие данные - прошу дополнить список. Ссылки по теме
|
|