о сложении (программистское)
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-09 08:49 am (UTC)no subject
Date: 2007-08-09 10:21 am (UTC)Хотя - если мне вдруг потребуется функция сложения большого количества чисел, то скорее всего напишу ее в лоб, и вспомню об этом только когда замечу расхождения с ожидаемым результатом. :(
no subject
Date: 2007-08-09 01:30 pm (UTC)Ho я не выпускник CS :-)
no subject
Date: 2007-08-09 01:41 pm (UTC)http://citeseer.ist.psu.edu/goldberg91what.html
no subject
Date: 2007-08-09 03:05 pm (UTC)Кстати, предлагаемый алгоритм на питоне для больших N ужасающе неэффективен на современных (Intel/AMD/PowerPC) процессорах – он ходит по памяти с большим шагом, эффективно вырубая L2 кэш...Считать количество сложений в наше время глупо – обращение к памяти медленнее любого сложения...
no subject
Date: 2007-08-13 01:35 pm (UTC)В реальности граздо эффективнее пользоваться при обработке floating point повышенной точностью (например, так построены стандартные библиотеки C) - потеря производительности будет значительно меньше. Ну, и есть упомянутый выше Кахан.
no subject
Date: 2007-08-13 02:50 pm (UTC)Абсолютно согласен. А за Кахана спасибо – не знал.
no subject
Date: 2007-08-10 09:11 pm (UTC)Решение с «равномерным» суммированием кажется мне совершенно неинтуитивным, я бы до него никогда не додумался. Алгоритм Кахана, который описан и в упоминаемой выше статье Голдберга, кажется мне гораздо более понятным — кажется совершенно естественным поддерживать переменную, хранящую текущее значение накопленной ошибки (значение этой переменной будет малым и проблем с порядком не возникнет).