Ошибка округления в среднем
У меня есть некоторые проблемы с ошибками округления в C++. Если мне нужно вычислить среднее значение двух поплавков a
а также b
тогда почему так лучше делать a+0.5*(b-a)
чем (a+b)/2
? Я не могу понять, почему должны быть какие-то различия в двух способах его вычисления.
2 ответа
Ваша формула верна в случае, если вы вычисляете среднее число многих чисел. В этом случае вы можете сделать следующее:
μn = 1 / nΣxi
но здесь при добавлении 101-го числа вам нужно будет добавить x101 к μ100, где μ100 может быть довольно большим по сравнению с x101, и поэтому вы потеряете некоторую точность. Чтобы избежать этой проблемы, вам может понравиться это:
μ101 = μ100 + 1 / n (x101 - μ100)
Эта формула будет намного лучше, если вы xi того же порядка, поскольку вы избегаете арифметических операций между двумя большими числами и xi.
Возможно, вы захотите прочитать статью Численно устойчивые вычисления арифметических средних
Давайте посмотрим, как числа представлены в IEEE с плавающей точкой. Рассмотрим C++ float
:
Интервал [1,2] идет с шага 2-23, поэтому вы можете представлять числа 1 + n * 2-23, где n принадлежит {0,..., 223}.
Интервал [2j, 2j + 1] соответствует значению [1,2], но умножается на 2j.
Чтобы увидеть, как теряется точность, вы можете запустить эту программу:
#include <iostream>
#include <iomanip>
int main() {
float d = pow(2,-23);
std::cout << d << std::endl;
std::cout << std::setprecision(8) << d + 1 << std::endl;
std::cout << std::setprecision(8) << d + 2 << std::endl; // the precision has been lost
system("pause");
}
Выход
1.19209e-07
1.0000001
2
[Отказ от ответственности: этот ответ предполагает формат и семантику IEEE 754. В частности, мы предполагаем, что float
это формат IEEE 754 binary32, в котором мы используем режим округления по умолчанию для округления до четности, и что промежуточные выражения не вычисляются с расширенной точностью - например, потому что FLT_EVAL_METHOD
является 0
.]
Вот одна из возможных причин предпочитать a + 0.5 * (b-a)
если a
а также b
очень большие и имеют тот же знак, то промежуточное количество a + b
в выражении 0.5 * (a + b)
может переполниться, давая либо бесконечный результат, либо исключение с плавающей точкой. По сравнению, a + 0.5 * (b - a)
не переполнится в этой ситуации.
Однако это небольшое преимущество следует сопоставить со следующим:
a + 0.5 * (b - a)
требует трех операций с плавающей точкой;0.5 * (a + b)
требуется только два.- в случаях, когда
a + b
не переполняется,0.5 * (a + b)
всегда дает правильно округленный ответ: то есть дает наилучшее возможное приближение к фактическому среднему значению, учитывая ограничения представимости целевого типа. (Это не совсем очевидно, но не трудно доказать: либоa + b
больше по величине, чем в два раза наименьшая нормальная, в этом случае сумма правильно округляется и умножение на0.5
является точным, илиa + b
вычисляется точно, а затем умножение на0.5
правильно округлено. В любом случае, не более одной из двух арифметических операций может привести к ошибке.) Ноa + 0.5 * (b - a)
не всегда будет давать правильно округленное среднее, и на самом деле может быть много миллионов язв по ошибке. Рассмотрим случай, когдаa = -1.0
а такжеb = 1.0 + 2^-23
, затемa + 0.5 * (b - a)
дает0.0
, Правильное среднее значение2^-24
, - выражение
a + 0.5 * (b - a)
может также переполниться, еслиa
а такжеb
очень большие с противоположными знаками, а не с тем же знаком. В этой ситуации0.5 * (a + b)
не переполнится. a + 0.5 * (b - a)
(очень немного) менее читабельно, чем0.5 * (a + b)
; читателю нужно немного подумать, чтобы понять, что он делает.
Учитывая вышесказанное, трудно поддержать общую рекомендацию a + 0.5 * (b - a)
следует использовать вместо 0.5 * (a + b)
,