Расширенная (80-битная) двойная плавающая точка в x87, а не в SSE2 — мы ее не пропустили?

Сегодня я читал о исследователи обнаружили, что библиотеки NVidia Phys-X используют x87 FP вместо SSE2. Очевидно, что это будет неоптимально для параллельных наборов данных, где скорость превосходит точность. Однако автор статьи продолжает цитировать:

Intel начала препятствовать использованию x87 с выпуском P4 в конце 2000 года. AMD отказалась от x87 с момента выпуска K8 в 2003 году, поскольку x86-64 определяется с поддержкой SSE2; VIA C7 поддерживает SSE2 с 2005 года. В 64-разрядных версиях Windows x87 устарел для пользовательского режима и полностью запрещен в режиме ядра. Почти все в отрасли рекомендуют SSE вместо x87 с 2005 года, и нет причин использовать x87, если только программное обеспечение не должно работать на встроенном Pentium или 486.

Я задумался об этом. Я знаю, что x87 использует 80-битные расширенные удвоения для вычисления значений, а SSE2 — нет. Разве это никому не важно? Мне это кажется удивительным. Я знаю, что когда я выполняю вычисления с точками, линиями и многоугольниками на плоскости, значения могут быть на удивление неправильными при выполнении вычитаний, а области могут разрушаться, а линии накладываться друг на друга из-за отсутствия точности. Думаю, может помочь использование 80-битных значений вместо 64-битных.

Это неправильно? Если нет, то что мы можем использовать для выполнения расширенных операций с двойным FP, если x87 будет прекращен?


person codekaizen    schedule 08.07.2010    source источник
comment
На самом деле это не ответ на ваш вопрос, но лично я надеюсь, что 128-битный двоичный формат IEEE 754 станет основным.   -  person Mark Dickinson    schedule 09.07.2010
comment
@Марк, серьезно, что так долго? AVX может стать стандартом до того, как он выйдет...   -  person codekaizen    schedule 09.07.2010
comment
Это хороший ответ на вопрос, в чем причина чтобы препятствовать x87. И да, вычисления SSE менее точны, это хорошо видно на современных JIT-компиляторах (по сравнению с традиционными компиляторами на базе x87).   -  person Egor Skriptunoff    schedule 08.06.2015


Ответы (4)


Самая большая проблема с x87 заключается в том, что все операции с регистрами выполняются в 80 битах, тогда как большую часть времени люди используют только 64-битные числа с плавающей запятой (т.е. числа с плавающей запятой двойной точности). Что происходит, так это то, что вы загружаете 64-битное число с плавающей запятой в стек x87, и оно преобразуется в 80-битное. Вы выполняете некоторые операции с ним в 80-битном формате, а затем сохраняете его обратно в память, преобразовывая в 64-битный формат. Вы получите другой результат, чем если бы вы выполнили все операции только с 64 битами, а с оптимизирующим компилятором может быть очень непредсказуемо, сколько преобразований может пройти значение, поэтому трудно проверить, что вы получаете " правильный" ответ при выполнении регрессионных тестов.

Другая проблема, которая имеет значение только с точки зрения того, кто пишет ассемблер (или косвенно пишет ассемблер, в случае, если кто-то пишет генератор кода для компилятора), заключается в том, что x87 использует стек регистров, тогда как SSE использует индивидуально доступные регистры. С x87 у вас есть куча дополнительных инструкций для управления стеком, и я полагаю, что Intel и AMD скорее заставят свои процессоры работать быстро с кодом SSE, чем пытаться заставить эти дополнительные инструкции x87 по управлению стеком работать быстро.

Кстати, если у вас возникли проблемы с неточностью, вам следует взглянуть на статью "Что каждый программист должен знать об арифметике с плавающей запятой", а затем, возможно, вместо этого использовать математическую библиотеку произвольной точности (например, GMP).

person tsuyoshi    schedule 10.07.2010
comment
Оптимизация компиляторов и так плоха, но попробуйте JIT, у которого есть возможность встраивать небольшие методы (и, следовательно, варьировать количество временных файлов в памяти). Иногда я вызываю этот метод и получаю один ответ, иногда я вызываю тот же метод с точно такими же аргументами и получаю другой результат, в зависимости от того, инлайнил ли JITter вызов или нет! Это была забавная регрессия, которую нужно было отследить. - person Joe White; 10.07.2010
comment
Да, я вижу, это усложняется, когда компиляторы делают такой выбор, тем более, когда это делают JIT-компиляторы. Что касается точности, в настоящее время я масштабирую число до [0..1] и удаляю общие биты, чтобы уменьшить шум из-за того, что биты просто отменяются, и просто представил, что 80 бит дадут мне больше места. Хотя это правда, по-видимому, побочные эффекты слишком дорого обходятся. Я надеюсь протестировать его на оборудовании QP... всякий раз, когда оно появится. - person codekaizen; 11.07.2010
comment
@Joe White Если вы используете Java и вам НУЖНЫ точно такие же результаты каждый раз, когда вы выполняете математику с плавающей запятой, исследуйте использование ключевого слова strictfp. Это заставляет математику быть IEEE 754, а не тем, что делает собственная платформа (например, x87 на 32b Intel). en.wikipedia.org/wiki/Strictfp - person KitsuneYMG; 10.01.2011
comment
@KitsuneYMG, на самом деле я использую .NET. Насколько я знаю, аналогов там нет. :( - person Joe White; 10.01.2011
comment
Стоит отметить, что 80-битная точность никогда не предназначалась для хранения. Он был специально разработан для использования в качестве промежуточного представления более высокой точности, которое будет преобразовано обратно в число с плавающей запятой или двойное число при сохранении результатов. - person ArchaeaSoftware; 23.12.2012
comment
Может ли что-нибудь помешать компилятору 8x87 сохранить все промежуточные результаты в виде 80-битных значений, независимо от того, помещаются они в регистры или нет, и указать, что он будет это делать? Не будут ли результаты такого компилятора полностью воспроизводимы любым другим компилятором, который делает то же самое? - person supercat; 12.09.2013
comment
@supercat Если компилятор x87 соответствует спецификации CLI, он должен усекать значения более высокой точности при наличии явной инструкции преобразования. Даже если мы не говорим о CLI, нужно определить промежуточный результат. Если функция возвращает двойное число, возвращаемое значение, по-видимому, не является промежуточным. Но что, если функция встроена? Разные компиляторы, по-видимому, будут принимать разные решения о встраивании. Если возвращаемое значение встроенной функции не нужно усекать, то разные компиляторы могут давать разные результаты. - person phoog; 10.12.2013
comment
@phoog: некоторые машины/компиляторы использовали 80-битную математику внутри себя, но произвольно преобразовывали значения в 64-битные double каждый раз, когда они не помещались в регистры, поэтому, если someDouble=f1()*f2()+f3()*f4() вычислялось в последовательности слева направо, оно могло округляться f1()*f2() в double, но не округлять f3()*f4() [поскольку больше не потребуется вызовов функций между моментом его вычисления и временем сохранения someDouble]. Такое поведение непристойно и противно. Но если бы правила округления не зависели от того, что помещается в регистры, а что нет, я бы не видел проблемы. - person supercat; 10.12.2013
comment
@phoog: Лично я хотел бы видеть язык с отдельными типами, например. ieee float, fast float и short real, где произведение двух чисел с плавающей запятой IEEE всегда округляется до float, а fast float округляется или не так удобно. short real будет 32-битным значением с плавающей запятой, но будет преобразовано в тип максимальной точности при выполнении математических операций, если такое преобразование может повысить точность результата [например, преобразование потребуется при вычислении f1=f2+f3+f4;, но не f1=f2+f3;]. - person supercat; 10.12.2013
comment
@phoog: Учитывая, что плавающие переменные используются по-разному, наличие разных типов для разных шаблонов использования позволило бы разработчикам языков предоставлять полезные предупреждения в случаях, когда программист, которому нужна строгая семантика одинарной точности IEEE, случайно умножает на 1,01, а не чем 1.01f, позволяя программисту, который хочет как можно точнее умножить число с плавающей запятой одинарной точности на 1,01, обойтись без уродливого приведения типов. - person supercat; 10.12.2013
comment
Обратите внимание, что x87 FPU на самом деле имеет контрольное слово, которое позволяет вам уменьшить внутреннюю точность до 64 бит или даже 32 бит, чтобы получить побитовые идентичные результаты, но, похоже, никто не использует это. - person fuz; 16.10.2017
comment
@fuz: по словам Брюса Доусона, MSVC раньше уменьшал до 64-бит (53-битный значащий) при запуске CRT. randomascii.wordpress.com/2012/03/21/ И DirectX, по-видимому, использовался для уменьшения точности до float для всего вашего процесса! - person Peter Cordes; 08.03.2019

Чтобы правильно использовать математику повышенной точности, необходимо, чтобы язык поддерживал тип, который может использоваться для хранения результатов промежуточных вычислений и может быть заменен выражениями, дающими эти результаты. Таким образом, учитывая:

void print_dist_squared(double x1, double y1, double x2, double y2)
{
  printf("%12.6f", (x2-x1)*(x2-x1)+(y2-y1)*(y2-y1));
}

должен быть какой-то тип, который можно было бы использовать для захвата и замены общих подвыражений x2-x1 и y2-y1, что позволяет переписать код как:

void print_dist_squared(double x1, double y1, double x2, double y2)
{
  some_type dx = x2-x1;
  some_type dy = y2-y1;
  printf("%12.6f", dx*dx + dy*dy);
}

без изменения семантики программы. К сожалению, ANSI C не смог определить какой-либо тип, который можно было бы использовать для some_type на платформах, выполняющих вычисления с повышенной точностью, и стало гораздо более распространенным обвинять Intel в существовании типов с повышенной точностью, чем обвинять ANSI в неудачной поддержке.

Фактически, типы повышенной точности имеют такое же значение на платформах без модулей с плавающей запятой, как и на процессорах x87, поскольку на таких процессорах вычисление типа x+y+z повлечет за собой следующие шаги:

  1. Распакуйте мантисса, экспонента и, возможно, знак x в отдельные регистры (экспонента и знак часто могут иметь двойную койку)
  2. Распакуйте аналогично.
  3. Сдвиньте вправо мантиссу значения с меньшим показателем, если таковой имеется, а затем добавьте или вычтите значения.
  4. Если x и y имеют разные знаки, сдвиньте мантиссу влево до тех пор, пока крайний левый бит не станет равным 1, и соответствующим образом отрегулируйте показатель степени.
  5. Упакуйте экспоненту и мантиссу обратно в двойной формат.
  6. Распаковать этот временный результат.
  7. Распаковать з.
  8. Сдвиньте вправо мантиссу значения с меньшим показателем, если таковой имеется, а затем добавьте или вычтите значения.
  9. В случае, если предыдущий результат и z имели разные знаки, сдвиньте мантиссу влево до тех пор, пока крайний левый бит не станет равным 1, и соответствующим образом отрегулируйте показатель степени.
  10. Упакуйте экспоненту и мантиссу обратно в двойной формат.

Использование типа повышенной точности позволит исключить шаги 4, 5 и 6. Поскольку 53-битная мантисса слишком велика, чтобы поместиться менее чем в четыре 16-битных или два 32-битных регистра, выполнение сложения с 64-битной мантиссой не медленнее, чем с 53-битной мантиссой, поэтому использование математика повышенной точности обеспечивает более быстрые вычисления без недостатков в языке, который поддерживает правильный тип для хранения временных результатов. Нет причин обвинять Intel в предоставлении FPU, который может выполнять математические операции с плавающей запятой способом, который был также наиболее эффективным методом на чипах без FPU.

person supercat    schedule 21.09.2015
comment
Верно, но я думаю, что мы можем обвинить Intel в том, что она не предоставила способ выполнения базовых арифметических операций с правильным округлением в соответствии со стандартами (на 64-битных двойниках) вообще. Да, вы можете изменить точность FPU на 53 бита вместо 64 бит, но это неуклюже, медленно, рискует помешать библиотечному коду, который ожидает 64-битную точность, и даже не решает проблему: в то время как это устраняет двойное округление в нормальный домен, он не меняет диапазон экспоненты, поэтому по-прежнему оставляет возможность двойного округления при потере значимости. SSE(2) в этом отношении является большим улучшением. - person Mark Dickinson; 23.09.2015
comment
@MarkDickinson: Хотя существуют специализированные приложения, требующие согласованного по битам поведения с плавающей запятой с операциями, включающими более короткие типы, для большинства приложений лучше иметь надлежащую поддержку повышенной точности. Я вижу, что SSE(2) и x87 служат разным целям, и мне бы хотелось, чтобы языки поддерживали их как активно продвигаемые, так и строгие типы с плавающей запятой; кроме того, выражения, включающие строгие типы, должны быть IMHO преобразованы в более крупные типы только после явного принуждения их к их собственному типу, поэтому, если f1 и f2 были строгими типами с плавающей запятой, d1=f1*f2... - person supercat; 23.09.2015
comment
... нужно было бы записать как d1=(float)(f1*f2); [не d1=(double)(f1*f2);!]. Я предполагаю, что в тех случаях, когда кто-то пишет d1=f1*f2;, очень высока вероятность того, что (1) код либо предназначен для того, чтобы сказать d1=(double)f1*f2;, (2) программист, который видит код, думает, что это означает это, либо (3) программист, который видит код, думает, что он имел в виду именно это. Требование, чтобы код был написан как d1=(float)(f1*f2); в тех случаях, когда предполагается такое поведение, устранило бы эти опасности. - person supercat; 23.09.2015
comment
но разве long double не является типом с расширенной точностью? - person marcin; 22.04.2016
comment
@marcin: Это так, и я бы предположил, что неприязнь многих людей к этому является следствием плохого отношения языков к этому. Цель разработки C заключалась в том, чтобы литералы без суффиксов были типом с наивысшей точностью, а аргументы функций с переменным числом переменных должны продвигаться к типу с наивысшей точностью, поэтому используйте такой код, как printf(%9.4f/%9.4f, x, y*Y_SCALE);` не нужно было бы беспокоиться о типе Y_SCALE, и даже если одно и то же значение Y_SCALE иногда использовалось в вычислениях float и double. Наличие типа long double, который не является взаимозаменяемым в printf, делает ситуацию неудобной, как и... - person supercat; 22.04.2016
comment
... имея объявление типа long double d=0.1;, установите d на 0,100000000000000000555, а не на 0,10000000000000000000813151629364. - person supercat; 22.04.2016

Другой ответ, кажется, предполагает, что использование 80-битной точности - плохая идея, но это не так. Иногда он играет жизненно важную роль в предотвращении неточностей, см., например, сочинения В. Кахана.

Всегда используйте 80-битную промежуточную арифметику, если вы можете обойтись без нее по скорости. Если это означает, что вы должны использовать математику x87, что ж, сделайте это. Его поддержка вездесуща, и пока люди продолжают поступать правильно, он останется вездесущим.

person Anonymous    schedule 21.09.2015
comment
Хотя, по иронии судьбы, промежуточная 64-битная точность (не 80-битная точность) от использования 80-битных регистров x87 может привести к менее точным результатам для простых арифметических операций. операции над обычными 53-битными двойниками. Предполагая обычный режим округления округления до четности, операция 1e16 + 2.9999 со значениями IEEE 754 binary64 дает правильно округленный результат 10000000000000002.0 на машине, использующей SSE2, но неправильно округленный результат 10000000000000004.0 при использовании x87 с точностью FPU не отличается от 64-битной точности по умолчанию благодаря двойному округлению. - person Mark Dickinson; 21.09.2015
comment
Есть несколько случаев, когда использование двойной точности для вычисления x+y дало бы результат с ошибкой округления 1/2ulp, в то время как использование расширенной точности и преобразование в двойную дало бы ошибку округления 2049/4096ulp. С другой стороны, гораздо больше случаев, когда использование расширенной точности для вычисления x+y+z даст точный результат, а использование double даст намного менее точный результат. в некоторых случаях просто неправильно. - person supercat; 22.09.2015

Двойная точность на 11 бит меньше, чем f80 (примерно 2,5 полубайта/цифры), для многих приложений (в основном игр) это не помешает. Но вам понадобится вся точность, доступная, скажем, для космической программы или медицинского приложения.

Это немного вводит в заблуждение, когда некоторые говорят, что f80 (и обескуражен этим) работает со стеком. Регистры FPU и операции, похожие на операции со стеком, возможно, это то, что сбивает людей с толку. Это на самом деле основано на памяти (загрузка/хранение), а не на стеке как таковом, по сравнению, например, с соглашением о вызовах, таким как cdecl stdcall, которое фактически передает параметры через стек. и в этом нет ничего плохого.

Большим преимуществом SSE на самом деле является операция сериализации, 2, 4, 8 значений одновременно, со многими вариационными операциями. Да, вы можете напрямую передавать в регистр, но в конце вы все равно перенесете эти значения в память.

Большим недостатком f80 является то, что его нечетная длина 10 байт нарушает выравнивание. вам придется выровнять их по 16 для более быстрого доступа. но не очень практично для массива.

Вам все равно придется использовать fpu для тригонометрических и других трансдентальных математических операций. Для asm есть много забавных и полезных трюков с f80.

Для игр и обычных простых приложений (почти всех) вы можете просто использовать двойное действие, не убивая никого. Но для нескольких серьезных, математических или научных приложений вы просто не можете отказаться от f80.

person user6801759    schedule 09.09.2016
comment
serialize operation. Вы имеете в виду параллельную работу. Или операция SIMD. - person Peter Cordes; 09.09.2016
comment
You still have to use fpu for trigonometric and other trancedental math operations. Если вы имеете в виду x87 FSIN, FYL2X (log2) и т. д., то нет, это неверно . Математические библиотеки реализуют эти функции в программном обеспечении с помощью математики SSE. - person Peter Cordes; 09.09.2016
comment
Еще до того, как x87 устарел, хорошие математические библиотеки не использовали FSIN, потому что внутреннее значение Pi, используемое для уменьшения диапазона, недостаточно точно; всего 66 бит. Intel не может изменить это по причинам обратной совместимости, но FSIN имеет большие ошибки около +/- pi/2 - person Peter Cordes; 09.09.2016
comment
да. Извините, я имел в виду параллель. Эмуляция всегда намного медленнее. на самом деле это мы делали до того, как появился числовой процессор. См. примечания Кахана по обоснованию дизайна IEEE 754 en.wikipedia.org/wiki/Floating_point#IEEE_754_design_rationale: Этот расширенный формат предназначен для использования с незначительной потерей скорости.. Но по прагматическим соображениям (более быстрая машина, большая емкость во всем), я думаю, никто больше не беспокоится о медленном и раздутом коде. - person user6801759; 10.09.2016
comment
О PI вы можете увидеть jpl.nasa.gov/edu/news/2016/3/16/ Многоточность, конечно, хороша, но она предназначена только для развлечения и упражнений. - person user6801759; 10.09.2016
comment
Программная эмуляция fsin не намного медленнее. Внутренняя реализация микрокодирована с 71-100 мкп (на Intel Haswell) с общей задержкой 47-106 циклов и (в данном случае) не делает ничего, чего нельзя было бы сделать с помощью простых инструкций x86, каждая из которых декодирует только одну uop. И что касается точности Pi, в статье, на которую вы ссылаетесь, ничего не говорится о катастрофической отмене или проблемах с плавающей запятой. Вы вообще читали статью Брюса Доусона, на которую я ссылался ранее? Вы слышали о катастрофической отмене? - person Peter Cordes; 10.09.2016
comment
Кстати, добро пожаловать в Stack Overflow. Вы должны отредактировать свое исправление (параллельно) в ответе. - person Peter Cordes; 10.09.2016