Свойства 80-битных вычислений повышенной точности, начиная с аргументов двойной точности

Вот две реализации функций интерполяции. Аргумент u1 всегда находится между 0. и 1..

#include <stdio.h>

double interpol_64(double u1, double u2, double u3)
{ 
  return u2 * (1.0 - u1) + u1 * u3;  
}

double interpol_80(double u1, double u2, double u3)
{ 
  return u2 * (1.0 - (long double)u1) + u1 * (long double)u3;  
}

int main()
{
  double y64,y80,u1,u2,u3;
  u1 = 0.025;
  u2 = 0.195;
  u3 = 0.195;
  y64 = interpol_64(u1, u2, u3);
  y80 = interpol_80(u1, u2, u3);
  printf("u2: %a\ny64:%a\ny80:%a\n", u2, y64, y80);
}

На строгой платформе IEEE 754 с 80-битными long double все вычисления в interpol_64() выполняются в соответствии с двойной точностью IEEE 754, а в interpol_80() - с 80-битной расширенной точностью. Программа печатает:

u2: 0x1.8f5c28f5c28f6p-3
y64:0x1.8f5c28f5c28f5p-3
y80:0x1.8f5c28f5c28f6p-3

Меня интересует свойство «результат, возвращаемый функцией, всегда находится между u2 и u3». Это свойство имеет значение false для interpol_64(), как показано значениями в main() выше.

Есть ли у собственности шанс соответствовать interpol_80()? Если нет, то какой контрпример? Помогает ли нам знать, что u2 != u3 или что между ними существует минимальное расстояние? Есть ли метод определения ширины значимой для промежуточных вычислений, при которой свойство гарантированно будет истинным?

РЕДАКТИРОВАТЬ: для всех случайных значений, которые я пробовал, свойство сохранялось, когда промежуточные вычисления выполнялись с повышенной точностью внутренне. Если бы interpol_80() принимало long double аргументов, было бы относительно легко построить контрпример, но здесь вопрос конкретно касается функции, которая принимает double аргументов. Это значительно усложняет построение контрпримера, если он есть.


Примечание: компилятор, генерирующий инструкции x87, может генерировать один и тот же код для interpol_64() и interpol_80(), но это не относится к моему вопросу.


person Pascal Cuoq    schedule 05.12.2012    source источник
comment
Вы уверены, что эта программа действительно использует 80-битную точность? Современные машины Intel / AMD IIRC имеют встроенные блоки 128 fp, поставляемые с SSE и друзьями.   -  person fuz    schedule 05.12.2012
comment
@FUZxxl «128-битные блоки FP» означают векторы двух чисел с двойной точностью или 4 чисел с одинарной точностью. Но отвечая на ваш вопрос, да, я уверен. Сборка находится здесь: pastebin.com/GaM20WZS   -  person Pascal Cuoq    schedule 05.12.2012
comment
+1 как для контента, так и для презентации   -  person R.. GitHub STOP HELPING ICE    schedule 05.12.2012


Ответы (2)


Да, interpol_80 () безопасна, давайте продемонстрируем это.

Проблема гласит, что входные данные 64-битные с плавающей запятой

rnd64(ui) = ui

Результат точно (при условии, что * и + - математические операции)

r = u2*(1-u1)+(u1*u3)

Оптимальное возвращаемое значение, округленное до 64-битного числа с плавающей запятой:

r64 = rnd64(r)

Поскольку у нас есть эти свойства

u2 <= r <= u3

Гарантируется, что

rnd64(u2) <= rnd64(r) <= rnd64(u3)
u2 <= r64 <= u3

Преобразование в 80 бит u1, u2, u3 тоже точное.

rnd80(ui)=ui

Теперь предположим, что 0 <= u2 <= u3, тогда выполнение с неточными операциями с плавающей запятой приводит не более чем к 4 ошибкам округления:

rf = rnd(rnd(u2*rnd(1-u1)) + rnd(u1*u3))

Предполагая округление до ближайшего четного, это будет не более чем на 2 ULP от точного значения. Если округление выполняется с помощью 64-битного числа с плавающей запятой или 80-битного числа с плавающей запятой:

r - 2 ulp64(r) <= rf64 <= r + 2 ulp64(r)
r - 2 ulp80(r) <= rf80 <= r + 2 ulp80(r)

rf64 может быть отключен на 2 ulp, поэтому interpol-64 () небезопасно, но как насчет rnd64( rf80 )?
Мы можем сказать, что:

rnd64(r - 2 ulp80(r)) <= rnd64(rf80) <= rnd64(r + 2 ulp80(r))

Начиная с 0 <= u2 <= u3, то

ulp80(u2) <= ulp80(r) <= ulp80(r3)
rnd64(u2 - 2 ulp80(u2)) <= rnd64(r - 2 ulp80(r)) <= rnd64(rf80)
rnd64(u3 + 2 ulp80(u3)) >= rnd64(r + 2 ulp80(r)) >= rnd64(rf80)

К счастью, как и любое число в диапазоне (u2-ulp64(u2)/2 , u2+ulp64(u2)/2), мы получаем

rnd64(u2 - 2 ulp80(u2)) = u2
rnd64(u3 + 2 ulp80(u3)) = u3

с ulp80(x)=ulp62(x)/2^(64-53)

Таким образом, мы получаем доказательство

u2 <= rnd64(rf80) <= u3

При u2 ‹= u3‹ = 0 мы можем легко применить то же доказательство.

Последний случай, который необходимо изучить, - это u2 ‹= 0‹ = u3. Если мы вычтем 2 больших значения, тогда результат может быть до ulp (big) / 2 off, а не ulp (big-big) / 2 ...
Таким образом, это утверждение, которое мы сделали, больше не работает:

r - 2 ulp64(r) <= rf64 <= r + 2 ulp64(r)

К счастью, u2 <= u2*(1-u1) <= 0 <= u1*u3 <= u3 и это сохраняется после округления

u2 <= rnd(u2*rnd(1-u1)) <= 0 <= rnd(u1*u3) <= u3

Таким образом, поскольку добавленные количества имеют противоположный знак:

u2 <= rnd(u2*rnd(1-u1)) + rnd(u1*u3) <= u3

то же самое происходит после округления, поэтому мы можем еще раз гарантировать

u2 <= rnd64( rf80 ) <= u3

QED

Для полноты мы должны позаботиться о ненормальных входных данных (постепенное истощение), но я надеюсь, что вы не будете так жестоки со стресс-тестами. Я не буду демонстрировать, что происходит с этими ...

ИЗМЕНИТЬ:

Вот продолжение, поскольку следующее утверждение было немного приблизительным и генерировало некоторые комментарии, когда 0 ‹= u2‹ = u3

r - 2 ulp80(r) <= rf80 <= r + 2 ulp80(r)

Мы можем записать следующие неравенства:

rnd(1-u1) <= 1
rnd(1-u1) <= 1-u1+ulp(1)/4
u2*rnd(1-u1) <= u2 <= r
u2*rnd(1-u1) <= u2*(1-u1)+u2*ulp(1)/4
u2*ulp(1) < 2*ulp(u2) <= 2*ulp(r)
u2*rnd(1-u1) < u2*(1-u1)+ulp(r)/2

Для следующей операции округления мы используем

ulp(u2*rnd(1-u1)) <= ulp(r)
rnd(u2*rnd(1-u1)) < u2*(1-u1)+ulp(r)/2 + ulp(u2*rnd(1-u1))/2
rnd(u2*rnd(1-u1)) < u2*(1-u1)+ulp(r)/2 + ulp(r)/2
rnd(u2*rnd(1-u1)) < u2*(1-u1)+ulp(r)

Для второй части суммы имеем:

u1*u3 <= r
rnd(u1*u3) <= u1*u3 + ulp(u1*u3)/2
rnd(u1*u3) <= u1*u3 + ulp(r)/2

rnd(u2*rnd(1-u1))+rnd(u1*u3) < u2*(1-u1)+u1*u3 + 3*ulp(r)/2
rnd(rnd(u2*rnd(1-u1))+rnd(u1*u3)) < r + 3*ulp(r)/2 + ulp(r+3*ulp(r)/2)/2
ulp(r+3*ulp(r)/2) <= 2*ulp(r)
rnd(rnd(u2*rnd(1-u1))+rnd(u1*u3)) < r + 5*ulp(r)/2

Я не доказал первоначальное утверждение, но не так уж и далеко ...

person aka.nice    schedule 05.12.2012
comment
Ваш ответ помогает мне более четко обдумать мой собственный вопрос, но кое-что я еще не понимаю. Когда я пытаюсь вычислить для себя границу разницы между математической версией выражения u2*(1-u1)+(u1*u3) и версией с плавающей запятой, я получаю ulp(u2) + ulp(u3) + ulp(u2 + u3), первый член - это ошибка u2*(1-u1), второй - ошибка (u1*u3), а третий - ошибка, вызванная продукт. Ваш результат 2 ulps кажется лучше, но я не уверен, как вы его получили ... - person Pascal Cuoq; 06.12.2012
comment
@PascalCuoq, вы правы, это было немного быстро ... При предположении 0 ‹= u2‹ = u3 все члены положительны и уступают по величине их сумме r, поэтому ulp (u2 * (1-u1)) + ulp (u3 * u1) ‹= 2 * ulp (r), а округление ограничено ulp / 2 после основных операций ... У вас также есть одна ошибка округления при выполнении rnd (1-u1) - person aka.nice; 06.12.2012
comment
О, что касается денормальных чисел, не нужно беспокоиться: когда u1, u2 и u3 являются числами с двойной точностью, тогда ни одно из подвыражений u2 * (1.0 - (long double)u1) + u1 * (long double)u3 не может быть long double денормальным. - person Pascal Cuoq; 06.12.2012
comment
Я не понимаю, почему утверждение, что ошибка в rf составляет не более 2 ULP, верно. Ошибка в 1-u1 может достигать ULP (1) / 4. Затем он умножается на u2, поэтому ошибка может достигать | u2 | • ULP (1) / 4. Эта ошибка переносится в RF. Если | u2 | огромна и rf мала, ошибка много ULP. - person Eric Postpischil; 06.12.2012
comment
Однако в свете этого доказательство можно зафиксировать. Если | u2 | ›› | r |, то очевидно, что u2 ≤ rnd (rf80). Поэтому нам просто нужно рассмотреть, действительно ли rnd (rf80) ≤ u3. Это вызывает беспокойство только в том случае, если r близко к u3, поэтому u1 близко к 1. Когда u1 близко к 1, 1-u1 является точным, поэтому ошибка этой операции равна нулю. - person Eric Postpischil; 06.12.2012
comment
@EricPostpischil согласен - эта часть была слишком приблизительной, я проверил границы ошибок более точно, это довольно привередливо, но поскольку u2 * rnd (1-u1) ‹= u2‹ = r и u2 * ulp (1) ‹= 2 * ulp (u2) мы все еще можем связать ошибку rnd (u2 * rnd (1-u1)) с несколькими ulp (r) - person aka.nice; 06.12.2012
comment
@ aka.nice: в double пусть u2 = -.1, u3 = 103, u1 = -u2 / (u3-u2). (Это значение для u1 вычисляется точно, затем округляется до двойного. U2 равно 0x1.999999999999ap-4, u1 равно 0x1.fc86155aa1659p-11.) Тогда r составляет примерно -4,342266507e-18, но вычисление его в длинном двойном дает примерно -4.343584954e-18 (точно -0x1.408p-58). Ошибка составляет около 1,367,432,361,508 ULP. Я думаю, что это больше, чем несколько, хотя у меня нет справки, подтверждающей это утверждение. - person Eric Postpischil; 06.12.2012
comment
@EricPostpischil true, но он попадает в ветку u2 ‹= 0‹ = u3, для которой я не указал границы ошибки - person aka.nice; 07.12.2012

Основным источником потери точности в interpol_64 является умножение. Умножение двух 53-битных мантисс дает 105- или 106-битную (в зависимости от того, переносит ли старший бит) мантиссу. Это слишком велико, чтобы уместиться в 80-битном значении расширенной точности, поэтому, как правило, у вас также будет потеря точности в 80-битной версии. Точно определить, когда это произойдет, очень сложно; самое простое - это то, что это происходит при накоплении ошибок округления. Обратите внимание, что при добавлении двух членов также есть небольшой шаг округления.

Большинство людей, вероятно, просто решат эту проблему с помощью такой функции, как:

double interpol_64(double u1, double u2, double u3)
{ 
  return u2 + u1 * (u3 - u2);
}

Но похоже, что вы ищете понимание проблем округления, а не лучшую реализацию.

person R.. GitHub STOP HELPING ICE    schedule 05.12.2012
comment
u1 - 0,025, а не 0,25, поэтому у него установлено больше бит, мантисса - 1999999999999a. - person Daniel Fischer; 05.12.2012
comment
@R .: u1 равно 0,025, а не 0,25; его мантисса (не мантисса) имеет более одного набора битов. И вопрос не в том, как изменить вычисление для получения результатов в диапазоне, вопрос в том, при каких обстоятельствах вычисление может быть вне диапазона. - person Eric Postpischil; 05.12.2012
comment
Я отредактировал свой ответ, чтобы он лучше соответствовал тому, что, похоже, ищет OP, но теперь это не очень удовлетворительный ответ. - person R.. GitHub STOP HELPING ICE; 05.12.2012
comment
Вопрос в контексте утверждения, что компиляторы с FLT_EVAL_METHOD = 0 лучше, потому что более предсказуемы. Имеющееся свойство затрудняет оспаривание этого, поскольку здесь оно ложно и истинно, когда компилятор молча компилирует interpol_64(), как если бы он был interpol_80() (скажем, компилятор FLT_EVAL_METHOD = 2, такой как современный GCC, нацеленный на x87, или I-безразлично компилятор, такой как старый GCC, нацеленный на x87). Вы правы, вопрос не об изменении исходного кода. Кроме того, разве u2 + u1 * (u3 - u2) не имеет такой же проблемы для хорошо выбранных u2, u3 и u1=1? - person Pascal Cuoq; 05.12.2012
comment
Я думаю, вы неверно истолковываете более предсказуемое. Слово «предсказуемый» в этом контексте не означает, что вы наивно ожидаете ответа. Это означает, что результаты не зависят от того, нужно ли компилятору / выбирает ли компилятор разлив регистров во время вычислений. FLT_EVAL_METHOD==2 непредсказуемо, потому что у вас нет возможности узнать или контролировать, может ли компилятор исчерпать временное пространство в регистрах с плавающей запятой и передать его в хранилище с номинальной точностью в стеке. - person R.. GitHub STOP HELPING ICE; 05.12.2012
comment
Что касается того, есть ли у u2 + u1 * (u3-u2) такая же проблема, по крайней мере, ничего не может пойти не так в случае u2==u3, который является наиболее патологическим случаем, который можно допустить. :-) Более того, если предположить, что u2 и u3 имеют одинаковый знак и величину (показатель степени), u3-u2 является точным, и умножение на u1 в указанном диапазоне никогда не может привести к выходу окончательного результата за пределы диапазона, независимо от режима округления. Единственный случай, требующий дальнейшего рассмотрения, - это когда u3 и u2 имеют разную величину. - person R.. GitHub STOP HELPING ICE; 05.12.2012
comment
Насколько я понимаю, FLT_EVAL_METHOD==2 гарантирует точность long double для всех промежуточных вычислений. Если промежуточное значение переливается, оно переливается в long double место стека. Вот как это делает современный GCC (я почти уверен). То, что вы описали, я назвал компилятором «мне наплевать». Мы согласны с тем, что это самое худшее. - person Pascal Cuoq; 05.12.2012
comment
Хм, я не знал, что GCC исправил это. Если это так, я думаю, что поведение программы, использующей плавающую точку, уникально / детерминировано для каждого значения FLT_EVAL_METHOD в {0, 1, 2} (непредсказуемо только для отрицательных значений, таких как FreeBSD). Это очень долгожданное событие. - person R.. GitHub STOP HELPING ICE; 05.12.2012
comment
Я не тестировал его (меня не очень интересует x87), но мой источник - gcc.gnu.org/ml/gcc-patches/2008-11/msg00105.html - person Pascal Cuoq; 05.12.2012
comment
Хорошо, поэтому не похоже, что есть какие-либо входные данные, которые заставляют вашу функцию фальсифицировать свойство из вопроса: свойство трудно получить, только когда u2 и u3 близки, а когда они близки, u3 - u2 является точным. Однако у вашей функции нет другого свойства, которое interpol(1.,u2,u3) всегда возвращает u3 (пример счетчика для u2=-DBL_EPSILON и u3=2). Функция в моем вопросе имеет это свойство. Возможно, поэтому он был написан так, как есть (я его не писал). - person Pascal Cuoq; 06.12.2012
comment
Надежная реализация, вероятно, выбирает версию в зависимости от того, близки ли u2 и u3 ... - person R.. GitHub STOP HELPING ICE; 06.12.2012