Фев 17 2015

FPU. Часть 1

Не задумывались ли вы о том, как работает FPU процессора? Казалось бы, там всё довольно просто. Есть стандарт представления чисел с плавающей точкой, IEEE754, а сама реализация арифметических операций очевидна.
Но на самом деле, за кажущейся простотой скрыто множество нюансов.

Во-первых, как это ни странно звучит, FPU оперирует не только с числами. Среди всех возможных значений кодов стандартом предусматриваются нечисла, выделенные значения, образующиеся в результате некоторых операций. Нечисла обозначаются как NaN (not a number). Например, нечисло образуется в результате деления ноля на ноль. Нечисло может иметь в знаковом разряде 0 или 1, однако особого смысла он не несёт, нечисла рассматриваются как беззнаковые. Еще одним выделенным значением является бесконечность. Бесконечность имеет знак, и может получаться как результат переполнения показателя степени числа, или, например, как результат деления ненулевого числа на ноль. Также числа могут быть нормализованными (и обычно ими и являются), и денормализованными. У денормализованного (subnormal, denormal) числа в разрядах экспоненты содержатся только нули, что соответствует минимально возможному показателю степени (2^-1022 для 64-битных чисел). Это крайне маленькие значения, порядка 10^-308, в реальном мире такие величины практически не применяются, поэтому в упрощенных реализациях FPU можно позволить себе некоторое отступление от стандарта и заменять их нулями (ноль, формально, тоже денормализованное число). И ещё, в стандарте IEEE754 нули имеют знак.

Вот и всё, что нам нужно знать для начала. За описанием стандарта добро пожаловать в википедию, здесь я его переписывать не буду.

Рассмотрим операцию деления, и начнем именно с «особых» значений. Для того, чтобы хорошо понять особенности работы FPU, я написал небольшую программу, которая выполняет деление различных чисел в 64-битном формате и выводит их двоичные коды. Вот что получилось:

0000000000000000 0000000000000000 fff8000000000000 //0 / 0 = nan
 0000000000000000 8000000000000000 fff8000000000000 //0 / -0 = nan
 0000000000000000 7ff8000000000000 7ff8000000000000 //0 / nan = nan
 0000000000000000 fff8000000000000 fff8000000000000 //0 / nan = nan
 0000000000000000 7ff0000000000000 0000000000000000 //0 / +inf = 0
 0000000000000000 fff0000000000000 8000000000000000 //0 / -inf = -0
 8000000000000000 0000000000000000 fff8000000000000 //-0 / 0 = nan
 8000000000000000 8000000000000000 fff8000000000000 //-0 / -0 = nan
 8000000000000000 7ff8000000000000 7ff8000000000000 //-0 / nan = nan
 8000000000000000 fff8000000000000 fff8000000000000 //-0 / nan = nan
 8000000000000000 7ff0000000000000 8000000000000000 //-0 / +inf = -0
 8000000000000000 fff0000000000000 0000000000000000 //-0 / -inf = 0
 7ff8000000000000 0000000000000000 7ff8000000000000 //nan / 0 = nan
 7ff8000000000000 8000000000000000 7ff8000000000000 //nan / -0 = nan
 7ff8000000000000 7ff8000000000000 7ff8000000000000 //nan / nan = nan
 7ff8000000000000 fff8000000000000 7ff8000000000000 //nan / nan = nan
 7ff8000000000000 7ff0000000000000 7ff8000000000000 //nan / +inf = nan
 7ff8000000000000 fff0000000000000 7ff8000000000000 //nan / -inf = nan
 fff8000000000000 0000000000000000 fff8000000000000 //nan / 0 = nan
 fff8000000000000 8000000000000000 fff8000000000000 //nan / -0 = nan
 fff8000000000000 7ff8000000000000 fff8000000000000 //nan / nan = nan
 fff8000000000000 fff8000000000000 fff8000000000000 //nan / nan = nan
 fff8000000000000 7ff0000000000000 fff8000000000000 //nan / +inf = nan
 fff8000000000000 fff0000000000000 fff8000000000000 //nan / -inf = nan
 7ff0000000000000 0000000000000000 7ff0000000000000 //+inf / 0 = +inf
 7ff0000000000000 8000000000000000 fff0000000000000 //+inf / -0 = -inf
 7ff0000000000000 7ff8000000000000 7ff8000000000000 //+inf / nan = nan
 7ff0000000000000 fff8000000000000 fff8000000000000 //+inf / nan = nan
 7ff0000000000000 7ff0000000000000 fff8000000000000 //+inf / +inf = nan
 7ff0000000000000 fff0000000000000 fff8000000000000 //+inf / -inf = nan
 fff0000000000000 0000000000000000 fff0000000000000 //-inf / 0 = -inf
 fff0000000000000 8000000000000000 7ff0000000000000 //-inf / -0 = +inf
 fff0000000000000 7ff8000000000000 7ff8000000000000 //-inf / nan = nan
 fff0000000000000 fff8000000000000 fff8000000000000 //-inf / nan = nan
 fff0000000000000 7ff0000000000000 fff8000000000000 //-inf / +inf = nan
 fff0000000000000 fff0000000000000 fff8000000000000 //-inf / -inf = nan

Здесь проверяется деление всех комбинаций значений: +0, -0, NaN, -NaN (как я уже писал, знаковый разряд не имеет особого смысла для NaN, но хотелось проверить все варианты), +Inf (бесконечность), -Inf. В общем, довольно логично. 0/0 = NaN, Inf/Inf = NaN, любая операция с NaN даёт в результате NaN. По умолчанию, NaN имеет 1 в знаковом разряде, то есть +0/+0 = (-)NaN, как ни странно, однако если NaN является одним из операндов, то процессор просто копирует его код в результат, никак не изменяя его. Это подтверждается и такими результатами:

bb5580277b40413e fff4d91fa8a5b9e5 fffcd91fa8a5b9e5 //-7.11395e-23 / nan = nan
7ffc143b0befe6ff 730a54668a546338 7ffc143b0befe6ff //nan / 1.43824e+246 = nan

Программа генерации тестового файла сначала перебирает все варианты, приведенные ниже, а затем генерирует 10000 пар случайных вещественных чисел и результат их деления. Если в результате получается денормализованное число, оно заменяется нулём с соответствующим знаком. Для операндов замены не производится.

Также я написал небольшую программу с GUI, «калькулятор», позволяющий производить операции с бинарным представлением чисел с плавающей точкой.Продолжение следует.

Ссылка на гитхаб: https://github.com/arktur04/FPU

В репозитории содержится исходный текст программы генерации тестового файла (на C++), сам тестовый файл, и программа-калькулятор, позволяющая переводить числа из hex-вида во float и наоборот и производить с числами арифметические действия.

По мере продвижения я буду пополнять репозиторий.

В следующий раз мы напишем простой тестбенч для операции деления.