Предположим, вам даны два набора переменных с плавающей запятой, реализованных в соответствии с IEEE754, которые должны рассматриваться как точные значения, рассчитанные в соответствии с формулами, представленными в стандарте. Все юридические значения возможны. Количество переменных в множестве может быть любым натуральным числом.
Что было бы хорошим способом сравнить точные, в математическом смысле, суммы значений, представленных указанными переменными. Из-за природы домена, проблема может быть легко представлена как сравнение одной суммы с нулем. Вы можете игнорировать возможность присутствия NaN или Infinities, так как это не имеет отношения к основной проблеме. (Эти значения можно легко и независимо проверить, и на них можно воздействовать способом, подходящим для конкретного применения этой проблемы.)
Наивным подходом было бы просто суммировать и сравнивать или суммировать значения одного набора и вычитать значения другого.
bool compare(const std::vector<float>& lhs, const std::vector<float>& rhs)
{
float lSum = 0.0f;
for (auto value : lhs)
{
lSum += value;
}
float rSum = 0.0f;
for (auto value : rhs)
{
rSum += value;
}
return lSum < rSum;
}
Совершенно очевидно, что существуют проблемы с наивным подходом, как упоминалось в различных других вопросах, касающихся арифметики с плавающей запятой. Большинство проблем связаны с двумя трудностями:
определенные порядки добавления определенных наборов значений могут привести к промежуточному переполнению (промежуточный результат вычислений выходит за пределы диапазона, поддерживаемого доступным типом данных)
float small = strtof("0x1.0p-126", NULL);
float big = strtof("0x1.8p126", NULL);
std::cout << std::hexfloat << small + big - big << std::endl;
std::cout << std::hexfloat << (big-2*small) + (big-small) + big - (big+small) - (big+2*small) << std::endl;
Этот код приведет к 0
а также inf
; это показывает, как порядок влияет на результат. Надеемся также, что проблема упорядочения нетривиальна.
float prev;
float curr = 0.0f;
do
{
prev = curr;
curr += strtof("0x1.0p-126", NULL);
} while (prev != curr);
std::cout << std::hexfloat << curr << std::endl;
Этот код, учитывая достаточное время для фактического завершения вычислений, приведет к 0x1.000000p-102
нет, как можно было наивно ожидать, 0x1.fffffep127
(Изменение инициализации curr на `strtof (» 0x1.fff000p-103 «) будет рекомендовано для фактического наблюдения за этим.); это показывает, как соотношение между промежуточными результатами добавления и конкретными добавлениями влияет на результат.
Много было сказано о получении наилучшей точности, например. этот вопрос.
Проблема под рукой отличается тем, что мы не хотим максимизировать точность, но у нас есть четко определенная функция, которую нужно точно реализовать.
Хотя для некоторых идея о том, что это полезное упражнение в лучшем случае кажется спорным, рассмотрим следующий сценарий: сравнение между этими наборами значений может стать краеугольным камнем других операций, выполняемых над целыми наборами данных независимо в различных средах. Синхронизированная, безупречная работа некоторых систем может зависеть от того, что это сравнение хорошо определено и детерминировано реализовано, независимо от порядка добавления и конкретной архитектуры, реализующей IEEE754 или нет.
Это или просто любопытство.
В обсуждении Алгоритм суммирования Кахана был упомянут как актуальный. Однако этот алгоритм является разумной попыткой минимизировать ошибку. Он не гарантирует правильного знака результата и не зависит от порядка операций (чтобы, по крайней мере, гарантировать непротиворечивый, если неверный результат, для перестановок множеств).
Одним из наиболее очевидных решений было бы использование / реализация арифметики с фиксированной запятой с использованием достаточного количества битов, чтобы точно представлять каждое возможное значение операнда и сохранять точный промежуточный результат.
Возможно, однако, это можно сделать, используя только арифметику с плавающей запятой таким образом, чтобы гарантировать правильный знак результата. Если это так, проблему переполнения (как показано в одном из приведенных выше примеров) необходимо решить в решении, поскольку этот вопрос имеет особый технический аспект.
(Далее следует оригинальный вопрос.)
У меня есть два набора нескольких значений с плавающей запятой (с плавающей или двойной). Я хочу дать идеальный ответ на вопрос, набор которого имеет большую сумму. Из-за артефактов в арифметике с плавающей запятой, в некоторых угловых случаях результат наивного подхода может быть неправильным, в зависимости от порядка операций. Не говоря уже о простой сумме, это может привести к переполнению.
Я не могу приложить никаких усилий с моей стороны, потому что все, что у меня есть, — это смутные идеи, все они сложные и неубедительные.
Одним из возможных подходов является вычисление суммы с использованием superaccumulator: это алгоритм для вычисления точных сумм чисел с плавающей точкой. Хотя эти идеи существуют уже давно, этот термин является относительно новым.
В некотором смысле вы можете думать об этом как о расширении суммирования Кахана, где последовательная сумма хранится как массив значений, а не просто как пара. Тогда основной задачей становится выяснить, как распределить точность между различными значениями.
Некоторые соответствующие документы и код:
Ю. К. Чжу и В. Б. Хейс. «Алгоритм 908: точное точное суммирование потоков с плавающей точкой». ACM Транзакции по математическому программному обеспечению (ACM TOMS), 37 (3): 37: 1-37: 13, сентябрь 2010. doi: 10,1145 / 1824801,1824815
Р. М. Нил, «Быстрое точное суммирование с использованием малых и больших супераккумуляторов». 2015. arXiv: 1505,05571
М. Т. Гудрич, А. Элдави «Параллельные алгоритмы суммирования чисел с плавающей точкой». 2016. arXiv: 1605,05436
Сообщение изначально было также написано на C, и мой код относится к этому.
Теперь я вижу, что пост предназначен только для C ++, но в следующем я не вижу ничего такого, что было бы неприменимо к C ++.
Упростить поиск знака суммы списка чисел ФП
Сравнение двух наборов чисел аналогично добавлению отрицания второго набора к первому и затем нахождению знака суммы объединенного списка. Этот знак отображается на >
, ==
или же <
из 2 оригинальных наборов.
Выполнять только точную математику FP
предположение: FP использует IEEE-подобные числа, включая субнормалы, основание 2, и является точным для определенных операций:
Добавление a +b
с одинаковым двоичным показателем и отличающимся знаком.
Вычитание того же знака 0.5 из числа в 0.5 <= |x| < 1.0
спектр.
ldexp*()
(разбить число на значимые и экспоненциальные части) функция возвращает точное значение.
Массив формы на экспоненту
Формируем массив сумм sums[]
чьи ценности будут только когда-либо (0 or 0.5 <= |sums[i]| < 1.0)
по одному для каждого возможного показателя и для некоторых показателей, превышающих макс. Более крупные необходимы для накопления |total_sum|
это превышает FP_MAX
, Это нужно до log2(SIZE_MAX)
больше элементов.
Добавить набор чисел в sums[]
Для каждого элемента набора номера, добавьте его в соответствующий sums[]
согласно его двоичному показателю. Это является ключевым моментом, так как может быть добавлено одинаковое число знаков и числа FP с разными знаками с общим двоичным показателем FP именно так. Добавление может привести к переносу с одинаковыми значениями знака и отмене с различными значениями знака — это обрабатывается. Входящий набор номеров не нужно сортировать.
нормировать sum[]
Для каждого элемента на ones[]
, убедитесь, что любые значения не 0,5, 0,0 или -0,5 уменьшены, оставшаяся часть добавлена к меньшему ones[]
,
Осмотреть sum[]
для самой значимой цифры
Наиболее значимый (ненулевой) one[s]
является признаком результата.
Приведенный ниже код выполняет задачу, используя float
как тип FP набора. Некоторые параллельные вычисления выполняются с использованием double
проверить на здравомыслие, но не способствует float
расчет.
Шаг нормализации в конце повторяется, как правило, дважды. Я подозреваю, что даже в худшем случае будет повторяться двоичная ширина float
signicand, около 23 раз.
Решение, кажется, о O(n)
, но использует массив размером с диапазон экспонент FP.
#include <assert.h>
#include <stdbool.h>
#include <float.h>
#include <stdio.h>
#include <time.h>
#include <stdint.h>
#include <stdlib.h>
#include <math.h>
#if RAND_MAX/2 >= 0x7FFFFFFFFFFFFFFF
#define LOOP_COUNT 1
#elif RAND_MAX/2 >= 0x7FFFFFFF
#define LOOP_COUNT 2
#elif RAND_MAX/2 >= 0x1FFFFFF
#define LOOP_COUNT 3
#elif RAND_MAX/2 >= 0xFFFF
#define LOOP_COUNT 4
#else
#define LOOP_COUNT 5
#endif
uint64_t rand_uint64(void) {
uint64_t r = 0;
for (int i = LOOP_COUNT; i > 0; i--) {
r = r * (RAND_MAX + (uint64_t) 1u) + ((unsigned) rand());
}
return r;
}
typedef float fp1;
typedef double fp2;
fp1 rand_fp1(void) {
union {
fp1 f;
uint64_t u64;
} u;
do {
u.u64 = rand_uint64();
} while (!isfinite(u.f));
return u.f;
}
int pre = DBL_DECIMAL_DIG - 1;void exact_add(fp1 *sums, fp1 x, int expo);
// Add x to sums[expo]
// 0.5 <= |x| < 1
// both same sign.
void exact_fract_add(fp1 *sums, fp1 x, int expo) {
assert(fabsf(x) >= 0.5 && fabsf(x) < 1.0);
assert(fabsf(sums[expo]) >= 0.5 && fabsf(sums[expo]) < 1.0);
assert((sums[expo] > 0.0) == ( x > 0.0));
fp1 half = x > 0.0 ? 0.5 : -0.5;
fp1 sum = (sums[expo] - half) + (x - half);
if (fabsf(sum) >= 0.5) {
assert(fabsf(sums[expo]) < 1.0);
sums[expo] = sum;
} else {
sums[expo] = 0.0;
if (sum) exact_add(sums, sum, expo);
}
exact_add(sums, half, expo+1); // carry
}
// Add x to sums[expo]
// 0.5 <= |x| < 1
// differing sign
void exact_fract_sub(fp1 *sums, fp1 x, int expo) {
if(!(fabsf(x) >= 0.5 && fabsf(x) < 1.0)) {
printf("%d %e\n", __LINE__, x);
exit(-1);
}
assert(fabsf(x) >= 0.5 && fabsf(x) < 1.0);
assert((sums[expo] > 0.0) != ( x > 0.0));
fp1 dif = sums[expo] + x;
sums[expo] = 0.0;
exact_add(sums, dif, expo);
}
// Add x to sums[]
void exact_add(fp1 *sums, fp1 x, int expo) {
if (x == 0) return;
assert (x >= -FLT_MAX && x <= FLT_MAX);
//while (fabsf(x) >= 1.0) { x /= 2.0; expo++; }
while (fabsf(x) < 0.5) { x *= (fp1)2.0; expo--; }
assert(fabsf(x) >= 0.5 && fabsf(x) < 1.0);
if (sums[expo] == 0.0) {
sums[expo] = x;
return;
}
if(!(fabsf(sums[expo]) >= 0.5 && fabsf(sums[expo]) < 1.0)) {
printf("%e\n", sums[expo]);
printf("%d %e\n", expo, x);
exit(-1);
}
assert(fabsf(sums[expo]) >= 0.5 && fabsf(sums[expo]) < 1.0);
if ((sums[expo] > 0.0) == (x > 0.0)) {
exact_fract_add(sums, x, expo);
} else {
exact_fract_sub(sums, x, expo);
}
}
void exact_add_general(fp1 *sums, fp1 x) {
if (x == 0) return;
assert (x >= -FLT_MAX && x <= FLT_MAX);
int expo;
x = frexpf(x, &expo);
exact_add(sums, x, expo);
}
void sum_of_sums(const char *s, const fp1 *sums, int expo_min, int expo_max) {
fp1 sum1 = 0.0;
fp2 sum2 = 0.0;
int step = expo_max >= expo_min ? 1 : -1;
for (int expo = expo_min; expo/step <= expo_max/step; expo += step) {
sum1 += ldexpf(sums[expo], expo);
sum2 += ldexp(sums[expo], expo);
}
printf("%-20s = %+.*e %+.*e\n", s, pre, sum2, pre, sum1);
}int test_sum(size_t N) {
fp1 a[N];
fp1 sum1 = 0.0;
fp2 sum2 = 0.0;
for (size_t i = 0; i < N; i++) {
a[i] = (fp1) rand_fp1();
sum1 += a[i];
sum2 += a[i];
}
printf("%-20s = %+.*e %+.*e\n", "initial sums", pre, sum2, pre, sum1);
int expo_min;
int expo_max;
frexpf(FLT_TRUE_MIN, &expo_min);
frexpf(FLT_MAX, &expo_max);
size_t ln2_size = SIZE_MAX;
while (ln2_size > 0) {
ln2_size >>= 1;
expo_max++;
};
fp1 sum_memory[expo_max - expo_min + 1];
memset(sum_memory, 0, sizeof sum_memory); // set to 0.0 cheat
fp1 *sums = &sum_memory[-expo_min];
for (size_t i = 0; i<N; i++) {
exact_add_general(sums, a[i]);
}
sum_of_sums("post add sums", sums, expo_min, expo_max);
// normalize
int done;
do {
done = 1;
for (int expo = expo_max; expo >= expo_min; expo--) {
fp1 x = sums[expo];
if ((x < -0.5) || (x > 0.5)) {
//printf("xxx %4d %+.*e ", expo, 2, x);
done = 0;
if (x > 0.0) {
sums[expo] = 0.5;
exact_add(sums, x - (fp1)0.5, expo);
} else {
sums[expo] = -0.5;
exact_add(sums, x - -(fp1)0.5, expo);
}
}
}
sum_of_sums("end sums", sums, expo_min, expo_max);
} while (!done);
for (int expo = expo_max; expo >= expo_min; expo--) {
if (sums[expo]) {
return (sums[expo] > 0.5) ? 1 : -1;
}
}
return 0;
}
#define ITERATIONS 10000
#define MAX_NUMBERS_PER_SET 10000
int main() {
unsigned seed = (unsigned) time(NULL);
seed = 0;
printf("seed = %u\n", seed);
srand(seed);
for (unsigned i = 0; i < ITERATIONS; i++) {
int cmp = test_sum((size_t)rand() % MAX_NUMBERS_PER_SET + 1);
printf("Compare %d\n\n", cmp);
if (cmp == 0) break;
}
printf("Success");
return EXIT_SUCCESS;
}
Бесконечности и NaN также могут быть обработаны, до некоторой степени, оставить это на потом.
Число с плавающей запятой, полученное в результате суммирования двух чисел с плавающей запятой, является только приближение. Дано я1 а также я2 Подводя итог, мы можем найти приближение ошибки в суммировании с плавающей точкой, выполнив это:
я1 + я2 знак равно я12
я12 — я2 знак равно я~ 1
я1 — я~ 1 знак равно яΔ
Ближайший приближение мы могли бы придумать для суммирования N числа будут вычислять ошибку для N — 1 операции сложения, а затем суммировать те N — 1 ошибки снова принимают N — 2. И вы повторите этот процесс N — 2 раз или пока все ошибки не ушли в 0.0
Есть пара вещей, которые можно сделать, чтобы ускорить вычисления ошибок до 0,0:
long double
Теперь вы можете оценить, насколько важна для вас точность. Я скажу вам, что в общем случае вычислительные затраты на вышеуказанную операцию возмутительны, учитывая результат, который вы получите все еще быть приближением.
Общепринятое решение Суммирование Кахана это счастливый брак между скоростью и точностью. Вместо того, чтобы удерживать ошибку до конца суммирования, Кахан будет прокручивать ее в каждом добавлении, предотвращая эскалацию ее значения за пределы диапазона с плавающей запятой с наивысшей точностью. Скажи, что нам дали vector<long double> i1
мы могли бы запустить Суммирование Кахана на нем следующим образом:
auto c = 0.0L;
const auto sum = accumulate(next(cbegin(i1)), cend(i1), i1.front(), [&](const auto& sum, const auto& input) {
const auto y = input - c;
const auto t = sum + y;
c = t - sum - y;
return t;
} ) - c;
Одна из возможностей выполнить это сравнение с уверенностью — создать класс для арифметики с фиксированной запятой точности, равной используемым типам и без ограничения по абсолютной величине.
Это может быть класс, реализующий следующие публичные методы:
FixedPoint(double d);
~FixedPoint();
FixedPoint operator+(const FixedPoint& rhs);
FixedPoint operator-(const FixedPoint& rhs);
bool isPositive();
(Для каждого поддерживаемого типа с плавающей точкой требуется отдельный конструктор.)
В зависимости от обстоятельств реализация потребует сбора bool
фиксированного, определенного размера или динамического размера; возможно std::bitset
, vector<bool>
или статический или динамический bool
массив.
Для простоты реализации я бы предложил реализовать кодировку дополнения 2.
Это очевидное и очень дорогое решение, которое ухудшило бы производительность, если бы это сравнение было ядром какой-либо системы. Надеюсь, есть лучшее решение.