Большинство устройств, вообще имеющих аппаратную поддержку вычислений с плавающей точкой, на данный момент поддерживают именно IEEE-754. Большая часть остальных случаев сводится к поддержке формата похожего на IEEE-754, но с некоторыми отличиями в семантике (наиболее часто - отсутствие NaN и/или бесконечностей, принудительное denormals-are-zero, неизменяемое направление округления к 0). Хорошо известными примерами являются PlayStation 2 и SPU (Synergistic Processing Unit) в PlayStation3 (см. напр. http://www-01.ibm.com/support/knowledgecenter/SSXHF6_10.1.0/com.i… sp_diffs.html ). Также не соответствует стандарту поддержка binary16 на некоторых ARM-устройствах.
Большинство устройств поддерживают binary32 и binary64 (float (single) и double соответственно).
Аппаратная поддержка binary128 чрезвычайно редка (примеры: z/Architecture и POWER9; в SPARC V8 и V9 заявлена поддержка, но реальная аппаратная поддержка отсутствует).
Аппаратная поддержка binary16 широко распространена в видеокартах (GPU) и также встречается в некоторых ARM-устройствах. В x86/x86_64 аппаратная поддержка binary16 (преобразование к другим типам с плавающей точкой и из них, но не арифметика) также присутствует в некоторых процессорах.
В x86/x86_64 также используется 80-битный расширенный формат Intel с явным ведущим знаком экспоненты.
Этот раздел далее в основном концентрируется на x86/x86_64.
Исторически, поддержка арифметики с плавающей точкой в x86 была реализована в (отдельном) сопроцессоре Intel 8087 (появившемся в 1980 г.). При отсутствии сопроцессора генерировалось прерывание, обработчик которого мог вызвать программную (медленную) реализацию соответствующей операции. Сопроцессор пережил несколько модификаций (80187, 80287, 80387), отличия в поведении которых, в основном, незначительны и представляют скорее исторический интерес. Начиная с Pentium отдельный сопроцессор отсутствует, и набор команд реализуется непосредственно в CPU.
В 1999 г. в процессоре Pentium III появилось расширение SSE, которое стало ещё одним способом проведения вычислений с плавающей точкой (в более раннем MMX были только целочисленные операции).
Данная статья не является учебником по языку ассемблера или архитектуре процессоров, поэтому ниже приводится довольно сжатое изложение представляющей интерес информации об аппаратной части.
x86 FPU включает в себя:
1. 8 регистров типа double extended размером 80 бит, организованных в стек.
2. Регистр состояния (Status register). Размер 16 бит.
3. Регистр управления (Control register). Размер 16 бит.
4. Регистр тегов (Tag word register). Размер 16 бит.
5. Регистр указателя последней инструкции (Last instruction pointer register). Размер 48 бит.
6. Регистр указателя последних данных (Last data (operand) pointer register). Размер 48 бит.
7. Регистр опкодов (Opcode register). Размер 10 бит.
Битовое представление 80-битного типа данных Intel x87 (double extended):
S
E 1 .. E15
M 0
M 1 M 2 .... M61 M62 M63
Его параметры:
+ Показать
− Скрыть
Параметр
Значение
k, размер в битах
80
p, точность в битах
64
emax, максимальная экспонента e
16383
bias (смещение), E − e
16383
бит знака
1
w, ширина экспоненты в битах
15
t, хвостовые биты экспоненты
63
Параметр
Значение
Максимальное значение
~1.2·10+4932
Минимальное положительное нормализованное значение
~3.4·10-4932
Минимальное положительное значение
~3.6·10-4951
Относительная ошибка
5.4·10-20 .. 1.0·10-19
Точно представимы все целые в диапазоне
-264 .. +264
Вычисления ведутся FPU только в этом формате. Запись в память и чтение из неё других форматов приводят к автоматическому преобразованию. При этом денормализованные числа binary32/binary64 нормализуются. Попытка чтения binary32/binary64 sNaN приводит к исключению; по умолчанию (исключение замаскировано) результатом операции при этом является qNaN.
Управляющее слово:
Биты:
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
Значение:
R
R
P
P
P
U
O
Z
D
I
Здесь:
I - маска Invalid Operation exception
D - маска Denormal Operand exception
Z - маска Zero Divide exception
O - маска Overflow exception
U - маска Underflow exception
P - маска Precision exception
PP - точность (00=24 bit, 01=reserved, 10=53 bit, 11=64 bit)
RR - направление округления (00=roundTiesToEven, 01=roundTowardNegative, 10=roundTowardPositive, 11=roundTowardZero)
Биты IDZOUP задают маски исключений.
При возникновении исключительной ситуации устанавливается соответствующий бит в слове состояния, и если исключение разрешено (соответствующий бит равен 0), то происходит исключение.
Исключения:
Invalid Operation - включает Invalid operation IEEE754 и переполнение/недополнение стека FPU.
Denormal Operand - арифметика над denormal числом, или загрузка его из памяти.
Zero Divide - соответствует Division by zero IEEE754.
Overflow - соответствует Overflow IEEE754 (обработчик вызывается только для записи/чтения в память).
Underflow - соответствует Underflow IEEE754 (обработчик вызывается только для записи в память)
Precision - соответствует Inexact IEEE754
Точность определяет только количество бит мантиссы, участвующих в вычислениях, но не диапазон экспонент. Неиспользуемые биты мантиссы содержат нули.
Незамаскированное исключение ведёт к вызову соответствующего обработчика. Но вызов этот происходит на следующей инструкции с плавающей точкой (точнее - на следующей инструкции wait-класса).
Подробнее см. разд. 8.4-8.6 1-го тома Intel® 64 and IA-32 Architectures Software Developer Manuals.
Пример см. https://randomascii.wordpress.com/2016/09/16/everything-old-is-ne… compiler-bug/
Слово состояния:
Биты:
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
Значение:
B
C3
T2
T1
T0
C2
C1
C0
E
S
P
U
O
Z
D
I
Здесь:
I - флаг Invalid Operation
D - флаг Denormal Operand
Z - флаг Zero Divide
O - флаг Overflow
U - флаг Underflow
P - флаг Precision
S - ошибка стека
E - незамаскированное исключение
C0-C3 - коды условий (condition codes)
TTT - верхний регистр стека
B - флаг занятости (для совместимости с 8087; отражает флаг E)
Флаги IDZOUP остаются поднятыми, пока не будут очищены явно.
C0-C3 выставляются инструкциями сравнения и некоторыми другими, и могут быть использованы для условных переходов.
Регистры последнего адреса хранят адрес последней инструкции и данных с плавающей точкой (16 бит селектора и 32 бита адреса (для 64-битного режима - младшие 32 бита)).
Это может быть полезно обработчикам исключений.
Регистр тегов содержит 8 полей, определяющих содержимое соответствующего регистра:
00 = число
01 = ноль
10 = особое значение (NaN, бесконечность, денормал)
11 = пуст (за пределами стека)
Регистр опкодов содержит опкод последней выполненной команды с плавающей точкой, в особом формате.
x86/x86_64: SIMD
В x86 SIMD включает:
1. 8 регистров XMM (128-битных). В 64-битном режиме регистров 16; начиная с расширений AVX - они 256-битные.
2. Регистр MXCSR (32-битный), содержащий управляющие биты и биты состояния.
Регистры XMM могут трактоваться как содержащие 32-битные или 64-битные (начиная с SSE2) значения с плавающей точкой; количество определяется размером регистра (128-битный регистр XMM содержит 4 числа типа binary32). Содержимое также может трактоваться как целые числа.
Регистр MXCSR:
Биты:
16-31
15
14
13
12
11
10
9
8
7
6
5
4
3
2
1
0
Значение:
reserved
ZF
CR[1]
CR[0]
PM
UM
OM
ZM
DM
IM
DAZ
PE
UE
OE
ZE
DE
IE
Здесь:
IE-PE - флаги исключений
DAZ - denormals are zero - все денормалы интерпретируются как 0 соответствующего знака; исключение не происходит
IM-PM - маски исключений
CR - направление округления (00=roundTiesToEven, 01=roundTowardNegative, 10=roundTowardPositive, 11=roundTowardZero)
ZF - flush to zero - операции приводящие к денормализованному результату возвращают 0 соответствующего знака; исключение не происходит
Быстродействие
До сих пор рассматривалась семантика операций, не обращая внимания на то, с какой скоростью они выполняются. Хотя значения могут отличаться (и весьма сильно) в зависимости от архитектуры, но несколько общих наблюдений в основном справедливы для большого набора машин (эти наблюдения являются упрощениями, т. к. детали в современных архитектурах достаточно сложны, и к тому же могут существенно отличаться; в основном рассматриваются x86/x86_64, на других архитектурах ситуация может отличаться):
1. Операции сложения, вычитания и умножения - быстрые. Они могут занимать 1 такт, и более того, за 1 такт могут выполняться несколько независимых операций. Это сравнимо со скоростью соответствующих целочисленных операций (и может быть быстрее). Скорость для binary32 и binary64 обычно одинакова.
2. Операция деления - более медленная (очень примерно - около 20 тактов; целочисленное деление обычно занимает чуть больше и выполняется тем же узлом, и, соответственно, может конфликтовать с вещественным за вычислительные ресурсы). Скорость типично зависит от размера операнда - для 32-битных чисел деление выполняется быстрее. Скорость квадратного корня - аналогична.
3. Математические функции FPU (exp, sin и т. д.) выполняются дольше. Конкретное время зависит от операции. Очень грубая оценка - 100 тактов. Скорость типично выше для более низкой, чем стандартная, точности.
4. Операции с NaN типично существенно медленнее. Конкретные значения сильно зависят от архитектуры, но замедление может достигать 2-х порядков (100 раз).
5. Операции с денормализованными числами типично существенно медленнее. Конкретные значения сильно зависят от архитектуры, но замедление может достигать 2 порядков (100 раз). Новые процессоры обычно чуть лучше справляются с денормализованными числами.