Алексей Михайличенко дата публикации 25-04-2006 03:36 Загадки округления. Тестирование функций округления различных языков.
Приложение к статье Загадки округления.
Чтобы поставить всех тестируемых в равные условия, вся работа велась через текстовые файлы. От программы требовалось считать файл с исходными данными, округлить числа, и построчно записать результат в другой файл. Таким образом, особенности внутреннего представления вещественных чисел на данном языке и соответствующих преобразований оставались на совести самой тестируемой программы.
Был подготовлен файл DATA.TXT с исходными данными: миллионом строк с числами с четырьмя знаками после запятой.
0.0000
0.0001
0.0002
0.0003
....
104164096.9996
104164096.9997
104164096.9998
104164096.9999
Числа охватывают диапазон от 0 до 100 миллионов. Предполагается, что этот диапазон значений наиболее интересен для учетно-бухгалтерских задач. Для исходных данных была подсчитана сумма неокругленных значений.
(Некоторые функции из-за особенностей реализации не могли принимать аргументы свыше 20 млн. Для них был подготовлен сокращенный вариант теста).
Затем были подготовлены образцовые файлы с результатами работы алгоритмов: арифметического (STD_A.TXT), бухгалтерского (STD_B.TXT), а также округления вверх и вниз, поскольку при некоторых SetRoundMode некоторые функции скатывались до этого. Для каждого алгоритма подсчитывалась эталонная сумма округленных значений.
Тестируемой програме, обещавшей реализовать один из алгоритмов, скармливался DATA.TXT. Полученный от нее файл RESULT.TXT сравнивался с образцовым STD_?.TXT соответствующего алгоритма (или с несколькими, если получалось ни то ни се). Считали количество ошибок (расхождений между файлами), и сумму округленных значений, полученных программой.
О чем говорит количество ошибок (графа 5 таблицы) ?
Ошибки возникали только при округлении пятерок. Учитывая, что в исходном DATA.TXT таких записей было 10 000, число ошибок, например, 5001 (строка 1), говорит о том, что лишь в половине случаев исследуемая программа совпадает с обещаемым алгоритмом. В графах 7, 8, 9 приведены примеры нехорошего поведения.
При наличии ошибок возникает второй вопрос — а куда клонит исследуемая программа сумму округленных значений? Ответ дает графа 6.
Как мы уже говорили, нам известна эталонная сумма "итого округленных" для каждого алгоритма. Для эталонного бухгалтерского она будет равна сумме неокругленных значений, как результат подавления статистической погрешности. Для арифметического — она будет на 50 руб. больше (так как 10,000 строк с пятерками дадут каждая по 0.5 коп. погрешности). Сравнив полученную сумму исследуемого алгоритма с эталонной, по полученной разнице можно сделать выводы:
- Если разница сумм (графа 6) соответствует по модулю количеству ошибок, умноженному на 0.01 (как в строке 1) то все ошибки совершались в одну сторону. Знак графы 6 показывает, куда именно.
- Если графа 6 меньше указанного в п.1, значит округление при отклонениях от образцового алгоритма происходило в разные стороны, частично (или полностью) компенсируя возникающее отклонение по сумме (строка 17).
- В пределе, если ошибки есть, но в результате сравнения с бухгалтерским алгоритмом разница получается 0 (или около того) (строка 2), это значит, что сумма округленных значений равна сумме неокругленных, то есть в результате работы программы все же происходит эффективное подавление статистической погрешности (ПСП), хотя и не бухгалтерским способом, а каким-то иным. Как видно, это довольно частый случай.
О чем еще хочется сказать. Характер распределения ошибок по числовой прямой для многих функций неравномерен: большие "чистые" участки перемежаются со всплесками ошибок. Видимо поэтому при выборочном тестировании авторами вручную ошибки не были замечены.
N |
Язык, модуль, функция
(с указанием номера вопроса на Круглом столе, если функция взята оттуда).
|
Алгоритм округления: Арифм., Бух, (?) - пытаемся сравнивать непонятное поведение функции
|
Кол-во ошибок (расхож- дений со сравнива- емым алгорит- мом)
|
Разница округленного итога с эталонной суммой алгоритма
|
Примеры ошибок
|
Примечание
|
Аргумент |
Должно быть по сравнива- емому алгоритму |
Получается (неверно) |
1
|
2
|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
Perl v5.8.2 built for cygwin-thread-multi-64int
|
1
|
встроенная, sprintf("%3.2f")
По документации sprintf вообще не обещает какого-либо внятного алгоритма округления. Тем лучше. Потестируем.
|
А(?) |
5001
|
-50.01
|
0.0150 0.0450 0.0750 0.1050
|
0.02 0.05 0.08 0.11
|
0.01 0.04 0.07 0.10
|
Нет, это явно не арифметическое округление. Большое кол-во расхождений (5001), большая накопленная разница (-50.01).
|
2
|
Б(?)
|
4801
|
-0.01 |
0.0050 0.0150 0.0250 0.0650
|
0.00 0.02 0.02 0.06
|
0.01 0.01 0.03 0.07
|
Налицо наличие алгоритма ПСП (разница -0.01, почти 0), но не бух. окр, а какого-то другого (большое кол-во расхождений с бух. окр. : 4801).
|
3
|
Math::Round::nearest(0.01, n)
|
А
|
1428
|
-14.28
|
129.0050 129.0150 129.0350 129.0450 |
129.01 129.02 129.04 129.05
|
129.00 129.01 129.03 129.04
|
Ошибки начинаются с чисел свыше 110, распределение очень интересно.
|
4 |
Math::Round::round(n*100)/100
|
А
|
0
|
0
|
|
|
|
OK. Вообще, обертки 100/100 всегда оказываются лучше, чем спец. функции с указанием знака.
|
5
|
Math::Round::round_even(n*100)/100
|
Б
|
0
|
0
|
|
|
|
OK. |
6
|
Math::Round::nearest_rand
|
Б
|
5000
|
35.72
|
|
|
|
Реализация экзотического алгоритмя Random Rounding.
На целых числах работает. Практически не работает на дробных значениях, преимущественно сваливается на Арифм. алгоритм. Пришлось обернуть...
|
7
|
Math::Round::nearest_rand(n*100)/100
|
Б
|
5037
|
0.53
|
|
|
|
Неплохо. Накопление погрешности подавлено практически полностью. Расхождения анализировать смысла нет — они случайные.
|
8
|
еще раз
|
Б
|
4929
|
0.57
|
|
|
|
FoxPro 2.6 DOS |
9
|
ROUND(n, 2) |
А
|
0
|
0
|
|
|
|
OK |
Excel 9.0.3821 SR-1
|
10
|
ОКРУГЛ(A1;2) |
А |
0 |
0
|
|
|
|
OK. Не зря считается эталоном в расчетах.
|
11 |
(VBA) Round(n1, 2)
|
Б
|
0
|
0
|
|
|
|
OK
|
12 |
(VBA) CLng(n1 * 100) / 100
|
Б
|
0
|
0
|
|
|
|
OK до 20 млн (больше не может)
|
QBASIC |
13 |
INT(n * 100 + .5) / 100
|
А(?)
|
4800 |
-48.00 |
0.0150 0.0450 0.0750 0.1050
|
0.02 0.05 0.08 0.11
|
0.01 0.04 0.07 0.10
|
Вообще-то по документации это чистый А...
|
Б(?) |
5000 |
2.00
|
0.0050 0.0150 0.0250 0.0650
|
0.00 0.02 0.02 0.06
|
0.01 0.01 0.03 0.07
|
...но неожиданно оказался с алгоритмом ПСП, опять же не бух. окр, но довольно успешным. |
14 |
CINT(n * 100) / 100
Был сделан отдельный тест до 20 млн. (больше оно не может).
|
Б
|
4368
|
0.00
|
0.0050 0.0150 0.0250 0.0650
|
0.00 0.02 0.02 0.06
|
0.01 0.01 0.03 0.07
|
Эффективнейшее ПСП, опять же неизвестное.
|
Delphi 7 Ent
|
SetRoundMode |
|
15 |
RoundTo
|
rmUp |
Округление вверх
|
8449
|
84.49
|
0.0300 0.0500 0.0600 0.0700
|
0.03 0.05 0.06 0.07
|
0.04 0.06 0.07 0.08
|
Излишне усердное
|
16 |
rmDown |
Округление вниз
|
776
|
-7.76
|
0.1300 0.2600 0.5100 0.5200 |
0.13 0.26 0.51 0.52
|
0.12 0.25 0.50 0.51
|
Излишне усердное |
17 |
rmNearest |
Б(?)
|
5003
|
-13.45
|
0.0150 0.0250 0.0550 0.0650
|
0.02 0.02 0.06 0.06
|
0.01 0.03 0.05 0.07
|
ПСП плохого качества, лишь в 50% совпадающее с Бух. окр.
|
18 |
SimpleRoundTo
|
rmUp |
А
|
1543
|
-15.43
|
0.0550 0.0950 0.1250 0.2550
|
0.06 0.10 0.13 0.26
|
0.05 0.09 0.12 0.25
|
Значительно врущее вниз
|
19 |
rmDown |
А
|
771
|
-7.71
|
0.0650 0.2550 0.2650 0.3150
|
0.07 0.26 0.27 0.32
|
0.06 0.25 0.26 0.31
|
врущее вниз, но ближе всего к ожидаемому Арифм. окр.
|
20 |
rmNearest |
А(?)
|
6344
|
-63.44
|
0.0150 0.0450 0.0550 0.0750
|
0.02 0.05 0.06 0.08
|
0.01 0.04 0.05 0.07
|
явно не оно
|
21 |
Б
|
5004
|
-13.44
|
0.0050 0.0150 0.0250 0.0550
|
0.00 0.02 0.02 0.06
|
0.01 0.01 0.03 0.05
|
Включился ПСП. Очень похоже на RoundTo
|
22 |
(Round(MyFloat*100))/100
Вопрос 17851, Светлов Виктор, Shabal
|
rmUp |
Округление вверх |
4798
|
47.98
|
0.0500 0.0700 0.0900 0.1000
|
0.05 0.07 0.09 0.10
|
0.06 0.08 0.10 0.11
|
Проблемы с числами на 00 — в половине случаев округляет неверно, и все вверх.
|
23 |
rmDown |
Округление вниз |
4802 |
-48.02
|
0.0100 0.0200 0.0300 0.0400
|
0.01 0.02 0.03 0.04
|
0.00 0.01 0.02 0.03
|
Та же проблема, только вниз.
|
24 |
rmNearest
|
Б
|
570
|
-0.04
|
0.2950 1.0550 1.0650 1.1750
|
0.30 1.06 1.06 1.18
|
0.29 1.05 1.07 1.17
|
Отличное ПСП, процесс на 96% совпадает с Бух. окр. Но 570 отличий все же наделало.
|
25 |
RoundTo(n + 0.0001, -2) Вопрос 36234, Shabal
|
rmNearest |
А
|
3655
|
36.55
|
0.0249 0.0349 0.0649 0.0849
|
0.02 0.03 0.06 0.08
|
0.03 0.04 0.07 0.09
|
Несколько рьяное Арифм. окр. Проблема с числами на xx49, но не со всеми, а в 37% случаев. (а что если еще меньшее прибавлять?)
|
26
|
RoundTo(n + 0.00001, -2)
|
rmNearest |
А
|
0
|
0.00
|
|
|
|
А если прибавлять еще меньше? OK. 1e-5...1e-8
|
27 |
RoundTo(n + 1e-10, -2);
|
rmNearest |
А
|
1272
|
-12.72
|
1185920.1150 1185920.1250 1185920.1350 1185920.1450
|
1185920.12 1185920.13 1185920.14 1185920.15
|
1185920.11 1185920.12 1185920.13 1185920.14
|
А если прибавлять еще меньше?
Ошибки начинаются примерно с 1 млн.
|
28 |
Функция от ЮГ (Вопрос 36234) Extended
|
rmNearest |
Б
|
4703
|
0.09
|
0.2950 1.0150 1.0350 1.0550
|
0.30 1.02 1.04 1.06
|
0.29 1.01 1.03 1.05
|
Еще одно неплохое ПСП, но опять же загадочное...
|
29 |
Оболочка вокруг Ceil, Вопрос 38097, Python
переполняется на 20 млн, т.к. Ceil: integer.. Отдельный тест.
|
rmUp |
Округление вверх |
8736
|
87.36
|
0.0100 0.0200 0.0300 0.0400
|
0.01 0.02 0.03 0.04 |
0.02 0.03 0.04 0.05 |
Почти все (87%) что на xx00 — переокругляет вверх.
|
30 |
rmDown |
Округление вверх |
0
|
0.00
|
|
|
|
OK. Парадоксально, но факт.
|
31 |
rmNearest |
Округление вверх |
771
|
7.71
|
0.0700 0.1400 0.2300 0.2800
|
0.07 0.14 0.23 0.28
|
0.08 0.15 0.24 0.29
|
Некоторые (8%) из тех что заканчиваются на 00 — переокругляет вверх. |
32 |
StrToFloat(FloatToStrF(n, ffFixed, 15, 2));
|
rmUp, rmNearest |
А
|
0
|
0.00 |
|
|
|
OK.
|
33 |
rmDown |
А(?)
|
4800
|
-48.00
|
0.0050 0.0150 0.0550 0.0650
|
0.01 0.02 0.06 0.07
|
0.00 0.01 0.05 0.06
|
|
34 |
Б(?)
|
5000 |
2.00 |
0.0150 0.0250 0.0450 0.0550 |
0.02 0.02 0.04 0.06 |
0.01 0.03 0.05 0.05 |
|
35
|
Trunc(n*100 + 0.5) / 100 (Вопрос 39942)
|
rmUp |
А |
0
|
0.00
|
|
|
|
OK.
|
36
|
rmDown |
А(?) |
4800
|
-48.00
|
0.0050 0.0150 0.0550 0.0650
|
0.01 0.02 0.06 0.07
|
0.00 0.01 0.05 0.06
|
|
37
|
Б(?) |
5000
|
2.00
|
0.0150 0.0250 0.0450 0.0550
|
0.02 0.02 0.04 0.06
|
0.01 0.03 0.05 0.05
|
|
38 |
rmNearest |
А |
571
|
-5.71
|
0.2650 0.2950 0.5250 0.5850
|
0.27 0.30 0.53 0.59
|
0.26 0.29 0.52 0.58
|
|
39
|
Вопрос 40789, функция N 1
|
rmNearest |
А
|
1278
|
-12.78
|
1.0050 1.0150 8.0650 8.0750
|
1.01 1.02 8.07 8.08
|
1.00 1.01 8.06 8.07
|
|
40 |
Вопрос 40789, функция N 2
|
rmNearest |
А
|
5088
|
-50.88
|
0.0150 0.0450 0.0750 0.1050
|
0.02 0.05 0.08 0.11
|
0.01 0.04 0.07 0.10
|
|
41 |
Вопрос 40789, функция N 1e (вариант, когда все заменено на extended)
|
rmNearest |
А |
1238
|
-12.38
|
8.0650 8.1550 129.1050 129.1350
|
8.07 8.16 129.11 129.14
|
8.06 8.15 129.10 129.13
|
заменой на Extended уничтожена разница между 1 и 2, но не устранена ошибка.
|
42 |
Вопрос 40789, функция N 2e
|
rmNearest |
А |
1238
|
-12.38
|
8.0650 8.1550 129.1050 129.1350
|
8.07 8.16 129.11 129.14
|
8.06 8.15 129.10 129.13
|
|
43 |
Вопрос 40789, функция N 3
|
меняется внутри самой функцией
|
А |
102
|
-1.02
|
1.0050 1.0150 8.0650 8.0750
|
1.01 1.02 8.07 8.08
|
1.00 1.01 8.06 8.07
|
настолько мало ошибок, что удивительно. Все таки алгоритм может быть либо верным, либо неверным, но чтобы неверным в 1% случаев...
|
44
|
Вопрос 40789, функция N 1e |
А |
50
|
-0.50
|
8.0650 8.1550 129.1050 129.1350
|
8.07 8.16 129.11 129.14
|
8.06 8.15 129.10 129.13
|
а как вам в 0.5 % ??? Если алгоритм сомнителен, в его результаты хоть иногда заглядывают и пересчитывают. А если он считается надежным, то его не проверяют и забывают. А он подгадит...
|
45
|
John Herbster DecimalRoundExt
|
меняется внутри самой функцией |
А
|
0
|
0
|
|
|
|
ОК
|
46
|
John Herbster DecimalRoundExt |
Б
|
0
|
0
|
|
|
|
OK. Вот достойный человек!
|
MySQL 3.23.58-nt
|
47 |
ROUND(n1, 2)
|
А
|
9428
|
-94.28
|
0.0050 0.0150 0.0250 0.0450
|
0.01 0.02 0.03 0.05
|
0.00 0.01 0.02 0.04
|
Алгоритм, завиcящий от системных библиотек.
xx50 округляет вниз, но не всегда, а в 94% случаев.
|
48 |
FLOOR(n1*100 + 0.5) / 100
|
А
|
570
|
-5.70
|
0.1450 0.2850 0.5650 0.5750
|
0.15 0.29 0.57 0.58
|
0.14 0.28 0.56 0.57
|
Почти хорошее арифм.округление, врет только в 5.7 % случаев, и все вниз.
|
49
|
FLOOR(n1*100 + 0.500001) / 100
|
А
|
0
|
0
|
|
|
|
ОК
|
PostgreSQL 7.4.8 on i686-pc-linux-gnu, compiled by GCC gcc (GCC) 3.3.5 20050117 (prerelease) (SUSE Linux)
|
50 |
ROUND(n1, 2)
|
А
|
0
|
0
|
|
|
|
ОК, также trunc(n1+0.005, 2)
|
|
[Математические функции]
Обсуждение материала [ 02-11-2009 03:55 ] 23 сообщения |