Минимизировать ошибку округления при делении на целые числа
Я пытаюсь сформировать число с плавающей запятой двойной точности (64-бит), взяв отношение одного произведения целых чисел, деленное на другое произведение целых чисел. Я хочу сделать это таким образом, чтобы уменьшить ошибку округления.
Я знаком с Кахан суммирования для сложения и вычитания. Какие методы работают для разделения?
числитель является произведением многих длинных значений (десятки тысяч), аналогично знаменателю. Я хочу предотвратить переполнение и и подток тоже. (Одно приложение оценивает бесконечные продукты, останавливаясь после достаточного количества терминов.)
одна вещь, которую я пробовал, - это разложить легко факторизуемые числа (используя пробное деление на известные простые числа до миллиона) и отменить общие факторы, что помогает, Но недостаточно. Мои ошибки приблизительно 1.0 E-13.
Я работаю на C#, но любой код, который работает со стандартными номерами с плавающей запятой IEEE, добро пожаловать.
исследования:
я наткнулся на хорошую статью, в которой обсуждается EFT (безошибочные преобразования) для + - x/, правило Хорнера (многочлены) и квадратный корень. Название- "4ccurate 4lgorithms in Floating Point 4rithmetic" Филиппа Ланглуа. Смотри http://www.mathematik.hu-berlin.de/~gaggle/S09/AUTODIFF/projects/papers/langlois_4ccurate_4lgorithms_in_floating_point_4rithmetic.pdf
вышеизложенное указало мне на карпа и Маркштейна (для деление):https://cr.yp.to/bib/1997/karp.pdf
4 ответов
какие методы работают для разделения?
отдела a/b
, вы можете оценить остаток (остаток):
a = b*q + r
этот остаток r
легко доступен, если вы сплавили-multiply-add
q = a/b ;
r = fma(b,q,-a) ;
тот же трюк fma может быть применен при умножении:
y = a*b ;
r = fma(a,b,-y) ; // the result is y+r
тогда, если вы получите два приблизительных операнда после продуктов (a0+ra) / (b0+rb)
, вы заинтересованы в (a0+ra) = q*(b0+rb) + r
.
Вы можете сначала оцените:
q0 = a0/b0 ;
r0 = fma(b0,q0,-a0);
затем приблизьте остаток как:
r = fma(q0,rb,r0-ra);
затем исправьте фактор как:
q = q0 + r/b0;
EDIT: что делать, если fma недоступен?
мы можем эмулировать fma, используя точное произведение à la Dekker, которое разлагается на точную сумму 2 с плавающей запятой, а затем трюк Болдо-Мелькиона roundToOdd, чтобы быть уверенным, что сумма 3 с плавающей запятой точно округлена.
но это будет переборщить. Мы используем fma только для оценки остаточной ошибки, поэтому мы обычно имеем c очень близко к-ab. В этом случае ab+c является точным, и у нас есть только 2 плавающие точки для суммирования, а не 3.
в любом случае, мы только приблизительно оцениваем остаточную ошибку группы операций, поэтому последний бит этого остатка не был бы таким важным.
таким образом, fma можно написать так:
/* extract the high 26 bits of significand */
double upperHalf( double x ) {
double secator = 134217729.0; /* 1<<27+1 */
double p = x * secator; /* simplified... normally we should check if overflow and scale down */
return p + (x - p);
}
/* emulate a fused multiply add: roundToNearestFloat(a*b+c)
Beware: use only when -c is an approximation of a*b
otherwise there is NO guaranty of correct rounding */
double emulated_fma(a,b,c) {
double aup = upperHalf(a);
double alo = a-aup;
double bup = upperHalf(b);
double blo = b-bup;
/* compute exact product of a and b
which is the exact sum of ab and a residual error resab */
double high = aup*bup;
double mid = aup*blo + alo*bup;
double low = alo*blo;
double ab = high + mid;
double resab = (high - ab) + mid + low;
double fma = ab + c; /* expected to be exact, so don't bother with residual error */
return resab + fma;
}
ну, немного менее излишне, чем генерал подражал fma, но возможно, было бы разумнее использовать язык, который предоставляет собственный fma для этой части работы...
эквивалент умножения суммирования Кахана, который вы ищете, - "двойное умножение". Здесь, Если ваши целые числа представимы как double
значения, функция Mul122
С crlibm в основном достаточно.
#define Mul122(resh,resl,a,bh,bl) \
{ \
double _t1, _t2, _t3, _t4; \
\
Mul12(&_t1,&_t2,(a),(bh)); \
_t3 = (a) * (bl); \
_t4 = _t2 + _t3; \
Add12((*(resh)),(*(resl)),_t1,_t4); \
}
bh
и bl
запущенный продукт хранится с дополнительной точностью как сумма двух double
значения. a
- следующее целое число (мы предполагаем, что оно точно преобразуется в double
). resh
и resl
получите следующий запущенный продукт, в котором фактор a
было принято во внимание.
для того, чтобы избежать underflow и переполнения, вы можете externalize показатель к целому числу ширины вы хотите. Это делается путем периодического применения frexp
функция к высокой части идущего продукта, и после этого нормализовать идущий продукт путем разделять оба компонента такой же силой 2 (отслеживающ полную силу 2 которой идущий продукт был разделен можно сделать сбоку с целочисленной переменной нужной ширины).
как часто применять frexp
зависит от того, связаны вы на целых множатся. Если целые числа ниже 253, что помогло бы им быть точно представленными как double
значения, вы можете сделать около 19 умножений, прежде чем нормализовать работающий продукт, потому что показатель двойной точности достигает 1023.
как только вы вычислите продукты, соответствующие числителю и знаменателю, отбрасывают низкие компоненты и делят высокие компоненты. Это приведет только к ошибке около 1ULP. Вы ведь не стремились к ошибке менее чем с двойной точностью, не так ли?
не забывайте о степенях двух, которые вы оставили на стороне как для числителя, так и для знаменателя! Вычесть их и применить разницу к частному с
деление не страдает от тех же катастрофических эффектов отмены, что и сложение и вычитание, а использование поплавков IEEE правильно округлено, и поэтому должно иметь относительную ошибку около 1/2 ulps (~2e-16). Любые ошибки, большие, чем это, скорее всего, являются результатом промежуточных продуктов, поэтому с ними нужно быть осторожным.
Деккер (1971) имеет некоторые алгоритмы для расширения точности элементарных математических операций: как указано другой ответ, они могут быть упрощены, если у вас есть доступ к операции fma.
другие ответы хороши, если у вас есть доступ к FMA (fused multiply-add), но C# не использует его. Я продолжаю искать быстрое решение, но я нашел правильное.
Шаг 1: Соберите числители и знаменатели отдельно.
Шаг 2: Снимите знак и подсчитайте, сколько множителей было отрицательным, чтобы узнать знак ответа.
Шаг 3: Цикл над всеми числами, вычисляя естественный журнал каждого.
Шаг 4: Накапливайте отдельные компенсированные суммы для логов числителей и знаменателей. (Используйте суммирование Кахана.)
Шаг 5: Возьмите разницу между двумя суммами и вычислите экспоненту.
Шаг 6: восстановите знак.
Я проверил это на 100 000 случайных целых чисел в числителе и те же числа в знаменателе, но с обоими наборами, перетасованными в другом случайном порядке. Если я использую наивный подход регулярного умножения и деления, мой накопительная ошибка составляет около 2x10^-15. Используя мой компенсированный подход к журналу, ошибка равна нулю. (Может, мне повезло?) Я буду делать больше испытаний более сложных случаев. Тем не менее, компенсируя сумму бревен, я получаю почти вдвое большую точность перед окончательным округлением.
Я удивлен, что это сработало так хорошо. Очевидно, что выполнение 200 000 логарифмов не является идеальным.
теория Примечание:
накопительная ошибка округления похожа на случайное блуждание. После n вычислений, вы можете ожидайте ошибку sqrt (N)*ULP/2. Если ULP / 2-5.0 E-18, а N-200,000, то вы получите 2.2 E-15, что близко к тому, что я получил для наивного подхода.