Июн 19 2015

FPU. Часть 5. Сумматор, тест и RTL

Итак, сумматор чисел в формате IEEE 754 с разрядностью 32 или 64 бита.
Исходники можно скачать на Github: https://github.com/arktur04/FPU.

КДПВ (кликабельно):

pic1

При разработке модуля суммирования (он же модуль вычитания) был обнаружен ряд «подводных камней», которые отличают его от других модулей FPU. В первую очередь, это касается тестов. При тестировании модуля умножения и модуля деления мы использовали сгенерированную программно таблицу случайных значений 64-битных целых чисел, которые затем переводились в соответствующие вещественные значения. Легко видеть, что при равномерном распределении исходных (целочисленных) значений мы получим случайные вещественные числа с равномерным распределением экспонент, то есть два случайных числа, оказавшихся в одной паре, скорее всего, будут различаться на много порядков. Это не мешает тестам умножителя и делителя, но для сумматора это означает, что результат, скорее всего, будет просто равен большему значению, так как меньшее просто выйдет далеко за пределы младшего знака большего числа.

Такие случаи тоже надо тестировать, конечно, но не они являются основным содержанием теста. Поэтому в тесте мы искусственно ограничиваем разброс двух операндов так, чтобы их экспоненты отличались не более чем на 53 (для 64-битных чисел). Это значение выбрано исходя из того, что 52 — длина мантиссы, и мы всё же оставляем некоторую вероятность (1/53) для «вырожденного» случая, когда одно из слагаемых меньше младшего разряда второго слагаемого.

    const unsigned MAX_EXP_DIFF = 53;
    ...
    for(int i = 0; i <= 10000; i++)
    {
    do
    {
        ll_x = randomInt64();
        ll_y = randomInt64();
    }
    while(abs(exponent(ll_x) - exponent(ll_y)) > MAX_EXP_DIFF);
    printLine(testfile, ll_x, ll_y, add);
}

Второй подводный камень оказался в системе округления результата. Для того, чтобы уменьшить потери точности на последнем знаке числа, во всех модулях FPU происходит стандартное округление, мы считаем на 1 разряд больше, и затем прибавляем к нему 1. В конечный результат этот разряд не записывается. Если в этом дополнительном разряде был ноль, то он станет равным единице, но на конечном результате это не отразится. Если дополнительный разряд был равен 1, то произойдёт перенос в следующий разряд, который является младшим битом результата. Следуя этой схеме, в модулях умножения и деления удалось добиться полного побитового совпадения результата с рассчитанным на компьютере. Однако при суммировании младший разряд в некоторых случаях ведёт себя иначе: иногда не происходит округление там, где оно должно быть. Понять закономерность мне так и не удалось, поэтому я решил, что единица младшего разряда, это не такая уж большая погрешность, и добавил в тест условие, по которому два числа, отличающиеся на один младший бит, считаются равными.

is_equal = ((expected == actual) || (expected == actual + 1) || (expected == actual - 1)) || (expectedIsNaN && actualIsNaN) || (expectedIsZero && actualIsZero);

Также равными считаются числа +0 и -0:

is_equal = ... (expectedIsZero && actualIsZero);

function is_zero;
input [`DATA_WIDTH - 1: 0] value;
begin
  is_zero = value[`DATA_WIDTH - 2: `DATA_WIDTH - `EXP_WIDTH - 1] == {`EXP_WIDTH{1'b0}};
end

Это происходит потому, что функция is_zero проверяет на равенство нулю все разряды, кроме value[`DATA_WIDTH — 1], который содержит знак.

В остальном поведение модуля полностью соответствует стандартному FPU компьютера, что подтверждается тестом их > 20.000 случайных пар чисел (10.000 для сложения и столько же для вычитания).

Итак, у нас готовы основные арифметические операции, и можно сделать что-либо более интересное. Например, модуль вычисления синуса/косинуса. Но об этом я напишу в следующий раз.