о сложении (программистское)
Aug. 8th, 2007 10:02 pmЧисла с плавающей точкой - вот еще одно в череде компьютерных задач, которыми я занимался мало и понимаю плохо. Сегодня я узнал, что числа с плавающей точкой надо складывать - если их много - не "наивно", а особенным образом. Мне неприятно, что я до сих пор этого не знал; или если знал, то забыл - кажется логичным, чтобы это упоминалось во время курса по численному анализу, который я брал на втором курсе много лет назад, но это был один из наиболее нелюбимых предметов, и после экзамена я с радостью забыл вообще все, что его касается.
Эта тема всплыла в одной внутренней рассылке на работе, где утверждалось, что средний выпускник CS-факультета этого не знает. Я уже давно не выпускник, но честно признаюсь: не знал. А вы, коллеги, знали?
Суть проблемы вот в чем. Предположим, вам надо суммировать миллион чисел с плавающей точкой, находящихся примерно в одном диапазоне - на расстоянии одного-двух порядков друг от друга. Тогда простой, как пробка, цикл от одного до миллиона с накоплением суммы - очень плохой способ суммировать эти числа. Дело в том, что вследствие того, что промежуточные суммы будут со временем намного больше среднего слагаемого, потери точности вследствие округления начнут играть заметную роль. Суммирование миллиона чисел таким образом легко может потерять 3-4 знака точности. Если, скажем, сложить таким образом миллион одинаковых чисел, а потом сравнить с результатом умножения исходного числа на миллион, это легко заметить.
В Перле:
Накопившаяся разница проявляется в шестом знаке после запятой, или, иными словами, четыре последних знака потеряны.
Что делать? В общем случае - помнить об ошибках накопления и учитывать их. В конкретном случае сложения большого количества примерно одинаковых чисел самым простым оказывается складывать их "равномерно". Вместо того, чтобы одна переменная копила сумму, сложим сначала пары чисел-соседей, и получим в два раза меньше промежуточных сумм. Их тоже сложим попарно, и так далее - log(N) шагов. Пример такого алгоритма можно увидеть на этой странице (написан на Питоне).
Эта тема всплыла в одной внутренней рассылке на работе, где утверждалось, что средний выпускник CS-факультета этого не знает. Я уже давно не выпускник, но честно признаюсь: не знал. А вы, коллеги, знали?
Суть проблемы вот в чем. Предположим, вам надо суммировать миллион чисел с плавающей точкой, находящихся примерно в одном диапазоне - на расстоянии одного-двух порядков друг от друга. Тогда простой, как пробка, цикл от одного до миллиона с накоплением суммы - очень плохой способ суммировать эти числа. Дело в том, что вследствие того, что промежуточные суммы будут со временем намного больше среднего слагаемого, потери точности вследствие округления начнут играть заметную роль. Суммирование миллиона чисел таким образом легко может потерять 3-4 знака точности. Если, скажем, сложить таким образом миллион одинаковых чисел, а потом сравнить с результатом умножения исходного числа на миллион, это легко заметить.
В Перле:
DB<1> $a=0.1234567890123456
DB<2> $x = 1_000_000*$a;
DB<3> for (1..1_000_000) { $y += $a; }
DB<4> x $y;
0 123456.789009695
DB<5> x $x
0 123456.789012346
DB<6> x $y-$x;
0 '-2.65109702013433e-06'
Накопившаяся разница проявляется в шестом знаке после запятой, или, иными словами, четыре последних знака потеряны.
Что делать? В общем случае - помнить об ошибках накопления и учитывать их. В конкретном случае сложения большого количества примерно одинаковых чисел самым простым оказывается складывать их "равномерно". Вместо того, чтобы одна переменная копила сумму, сложим сначала пары чисел-соседей, и получим в два раза меньше промежуточных сумм. Их тоже сложим попарно, и так далее - log(N) шагов. Пример такого алгоритма можно увидеть на этой странице (написан на Питоне).
no subject
Date: 2007-08-08 07:35 pm (UTC)т.е.
(4.2 - 2.2)^2 не всегда равно (5.2-3.2)^2
Факт очевидный, но на практике всегда о нем зыбываешь.
no subject
Date: 2007-08-08 07:39 pm (UTC)no subject
Date: 2007-08-10 08:52 pm (UTC)no subject
Date: 2007-08-09 06:40 am (UTC)Нельзя сравнивать на "равенство".
А сравнивать на "больше"/"меньше" допустимо.
no subject
Date: 2007-08-09 08:43 am (UTC)no subject
Date: 2007-08-09 08:54 am (UTC)no subject
Date: 2007-08-12 07:41 pm (UTC)no subject
Date: 2007-08-10 08:50 pm (UTC)no subject
Date: 2007-08-12 07:42 pm (UTC)no subject
Date: 2007-08-12 07:06 pm (UTC)Или нет, не рассказывайте про машину. Просто объясните тёмным, как это возможно в принципе.
no subject
Date: 2007-08-12 07:39 pm (UTC)no subject
Date: 2007-08-12 08:28 pm (UTC)no subject
Date: 2007-08-13 06:40 am (UTC)А использование равенства, подразумевает получение точного результата.
no subject
Date: 2007-08-13 02:36 pm (UTC)while ( a < b ) {
....
}
в программистском (практическом) смысле ничем не отличается от
while ( b - a > epsilon ) {
...
}
Так что сравнивать плавающие числа на неравенство всё-таки можно.
no subject
Date: 2007-08-13 06:05 pm (UTC)you must be kidding me.
no subject
Date: 2007-08-13 07:56 pm (UTC)Действительная проблема со сравнением на равенство состоит в том, что из-за ошибок, всегда присутствующих в вычислениях с плавающей точкой, сравнение на равенство почти всегда (за исключением тривиальных случаев меры ноль) оказывается не выполненным т.е. равенство просто не является подходящим отношением эквивалентности для сравнения вещественных чисел. [Здесь есть одно важное исключение — сравнение на равенство нулю, которое, как ни странно, в корне отличается от сравнения с ненулевым числом и всегда корректно.] В отличие от равенства сравнение на больше/меньше не доставляет таких неприятностей — хотя на границе действительно наблюдается некоторый «дребезг», тем не менее, при таких сравнениях отсутствует необходимость в дополнительных ухищрениях.
no subject
Date: 2007-08-13 04:55 pm (UTC)Кстати, легко привести пример таких чисел a и p, что неравенство p > 0 выполнено, но неравенство a + p > a уже не выполнено. Если Вы подумаете секунду, то сами поймете, что существование таких чисел совершенно очевидно.