Какой хороший способ сложить вместе большое количество маленьких поплавков?

Скажем, у вас есть 100000000 32-битных значений с плавающей запятой в массиве, и каждое из этих значений с плавающей запятой имеет значение от 0,0 до 1,0. Если вы попытаетесь суммировать их все так

result = 0.0;
for (i = 0; i < 100000000; i++) {
    result += array[i];
}

вы столкнетесь с проблемами, так как result становится намного больше, чем 1.0.

Итак, каковы некоторые из способов более точного выполнения суммирования?


person splicer    schedule 16.03.2010    source источник
comment
почему вы ожидаете, что результат будет меньше 1? Я смущен!   -  person lexu    schedule 16.03.2010
comment
Я думаю, он говорит, что как только результат достигает 1.0, начинают возникать проблемы. Какие проблемы я не знаю, но я так это воспринял.   -  person Adam Robinson    schedule 16.03.2010
comment
В Python используйте math.fsum (docs.python.org/library/math.html# math.fsum).   -  person kennytm    schedule 16.03.2010
comment
Я думаю, из примера кода мы можем предположить, что это не Python.   -  person Ken    schedule 16.03.2010
comment
@splicer: Можете ли вы быть более конкретным - какие «проблемы» вы имеете в виду?   -  person ire_and_curses    schedule 16.03.2010
comment
Я пытаюсь избежать накопления числовых ошибок. Статья в Википедии, на которую ссылается ответ Дэниела Прайдена, дает хорошее объяснение проблемы.   -  person splicer    schedule 16.03.2010
comment
Мне кажется, что по-прежнему будет значительная потеря точности, если предположить, что поплавки что-то представляют (и, следовательно, не могут считаться точными). С числами с плавающей запятой 1E8 я ожидаю, что ошибка будет примерно в 1E4 раз больше средней ошибки, а это означает, что многие значащие цифры будут накапливаться.   -  person David Thornley    schedule 16.03.2010


Ответы (5)


Похоже, вы хотите использовать суммирование Кахана.

Согласно Википедии,

Алгоритм суммирования Кэхана (также известный как компенсированное суммирование) значительно снижает числовую ошибку в сумме, полученной путем добавления последовательности чисел с плавающей запятой конечной точности, по сравнению с очевидным подходом. . Это делается путем сохранения отдельной рабочей компенсации (переменная для накопления небольших ошибок).

В псевдокоде алгоритм такой:

function kahanSum(input)
 var sum = input[1]
 var c = 0.0          //A running compensation for lost low-order bits.
 for i = 2 to input.length
  y = input[i] - c    //So far, so good: c is zero.
  t = sum + y         //Alas, sum is big, y small, so low-order digits of y are lost.
  c = (t - sum) - y   //(t - sum) recovers the high-order part of y; subtracting y recovers -(low part of y)
  sum = t             //Algebraically, c should always be zero. Beware eagerly optimising compilers!
 next i               //Next time around, the lost low part will be added to y in a fresh attempt.
return sum
person Daniel Pryden    schedule 16.03.2010
comment
Как раз то, что я искал! Спасибо :) - person splicer; 16.03.2010
comment
Мне сказали, что вы должны быть осторожны с оптимизацией компилятора, которая может выполнять перегруппировку операций и предполагать правила ассоциативности, которые неверны в ситуациях потери значимости. Возможно, вам придется просмотреть промежуточный код или сборку для проверки. Строки кода, которые может сломать компилятор: t = sum + y и c = (t - sum) - y. В арифметике с бесконечной точностью (t - сумма) будет точно равно y, а c всегда будет равно нулю. - person Paul Chernoch; 15.05.2012
comment
@PaulChernoch: Да, некоторые оптимизации компилятора могут сломать это (в одном из комментариев даже указано: остерегайтесь нетерпеливой оптимизации компиляторов!). В gcc все должно быть хорошо, если вы не используете --ffast-math. (Этот флаг намеренно нарушает некоторые гарантии, предоставляемые IEEE-754, поэтому, насколько я знаю, он никогда не включается, если вы не попросите об этом явно). Насколько я знаю, ни один компилятор по умолчанию не выполняет оптимизацию, предполагающую арифметику с бесконечной точностью, именно потому, что такие операции будут нарушены. - person Daniel Pryden; 16.05.2012

Сделайте результат двойным, предполагая C или C++.

person Tuomas Pelkonen    schedule 16.03.2010
comment
Да, это поможет, но что, если вам нужно суммировать гораздо больше, чем 100000000 значений? Мой выбор 100000000 для этого вопроса был произвольным. - person splicer; 16.03.2010

Если вы можете терпеть немного дополнительного места (в Java):

float temp = new float[1000000];
float temp2 = new float[1000];
float sum = 0.0f;
for (i=0 ; i<1000000000 ; i++) temp[i/1000] += array[i];
for (i=0 ; i<1000000 ; i++) temp2[i/1000] += temp[i];
for (i=0 ; i<1000 ; i++) sum += temp2[i];

Стандартный алгоритм «разделяй и властвуй», в общем. Это работает, только если числа разбросаны случайным образом; это не сработает, если первые полмиллиарда чисел равны 1e-12, а вторые полмиллиарда намного больше.

Но прежде чем делать что-либо из этого, можно просто накопить результат в двойнике. Это очень поможет.

person Rex Kerr    schedule 16.03.2010

Если в .NET используется метод расширения LINQ .Sum(), существующий в IEnumerable. Тогда это будет просто:

var result = array.Sum();
person Rob Packwood    schedule 16.03.2010
comment
Спасибо, но я должен быть более конкретным: я работаю на C и OpenCL. - person splicer; 16.03.2010
comment
Это также не решает проблему накопления ошибок. - person recursive; 09.05.2012

Абсолютно оптимальный способ — использовать приоритетную очередь следующим образом:

PriorityQueue<Float> q = new PriorityQueue<Float>();
for(float x : list) q.add(x);
while(q.size() > 1) q.add(q.pop() + q.pop());
return q.pop();

(этот код предполагает, что числа положительные; обычно очередь должна быть упорядочена по абсолютному значению)

Объяснение: дан список чисел, чтобы сложить их как можно точнее, нужно стремиться к тому, чтобы числа были близкими, т.е. устранить разницу между маленькими и большими. Вот почему вы хотите сложить два наименьших числа, тем самым увеличив минимальное значение списка, уменьшив разницу между минимумом и максимумом в списке и уменьшив размер задачи на 1.

К сожалению, я понятия не имею, как это можно векторизовать, учитывая, что вы используете OpenCL. Но я почти уверен, что это может быть. Вы можете взглянуть на книгу о векторных алгоритмах, удивительно, насколько они на самом деле эффективны: Векторные модели для параллельных вычислений

person jkff    schedule 17.03.2010
comment
На самом деле это не оптимальное решение. Вы хотите минимизировать абсолютное значение промежуточных результатов, что не обязательно означает, что вы всегда должны сначала добавлять наименьшие числа. Например, если вы хотите суммировать [1,01, -0,001, -1,02, 0,0012], лучше всего выразить это как (0,0012 - 0,001) + (1,01 - 1,02). - person quant_dev; 20.04.2010