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

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

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

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

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

Ответ 1

Похоже, вы хотите использовать Kahan Summation.

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

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

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

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

Ответ 2

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

Ответ 3

Если вы можете терпеть небольшое дополнительное пространство (в 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];

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

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

Ответ 4

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

var result = array.Sum();

Ответ 5

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

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();

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

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

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