Оптимизированная низкоточная аппроксимация `rootn (x, n)`

rootn (float_t x, int_t n) - это функция, которая вычисляет n-й корень x 1/n и поддерживается некоторыми языками программирования, такими как OpenCL. Когда используются числа с плавающей запятой IEEE-754, эффективные низкоточные стартовые аппроксимации для любого n могут быть сгенерированы на основе простой манипуляции базовым шаблоном бит, предполагая, что нужно обрабатывать только нормализованные операнды x.

Двоичный показатель root (x, n) будет равен 1/n бинарного показателя x. Поле экспоненты числа с плавающей запятой IEEE-754 является смещенным. Вместо того, чтобы не смещать экспонента, делить его и повторно смещать результат, мы можем просто делить смещенный показатель на n, а затем применить смещение, чтобы компенсировать ранее забытое смещение. Кроме того, вместо выделения, разделив поле экспоненты, мы можем просто делить весь операнд x, повторно интерпретируемый как целое число. Требуемое смещение тривиально, чтобы найти в качестве аргумента значение 1, вернет результат 1 для любого n.

Если у нас есть две вспомогательные функции, __int_as_float(), которые переинтерпретируют IEEE-754 binary32 как int32 и __float_as_int(), которые переинтерпретируют операнд int32 как binary32, мы приходим к следуя низкоточному приближению к rootn (x, n) простым образом:

rootn (x, n) ~= __int_as_float((int)(__float_as_int(1.0f)*(1.0-1.0/n)) + __float_as_int(x)/n)

Целочисленное деление __float_as_int (x) / n может быть сведено к сдвигу или умножению плюс сдвиг известных оптимизаций целочисленного деления на константный делитель. Некоторые примеры:

rootn (x,  2) ~= __int_as_float (0x1fc00000 + __float_as_int (x) / 2)  // sqrt (x)
rootn (x,  3) ~= __int_as_float (0x2a555556 + __float_as_int (x) / 3)  // cbrt (x)
rootn (x, -1) ~= __int_as_float (0x7f000000 - __float_as_int (x) / 1)  // rcp (x)
rootn (x, -2) ~= __int_as_float (0x5f400000 - __float_as_int (x) / 2)  // rsqrt (x)
rootn (x, -3) ~= __int_as_float (0x54aaaaaa - __float_as_int (x) / 3)  // rcbrt (x)

При всех этих аппроксимациях результат будет точным только тогда, когда x= 2 n * m для целых m. В противном случае аппроксимация обеспечит переоценку по сравнению с истинным математическим результатом. Мы можем приблизительно вдвое уменьшить максимальную относительную погрешность, слегка уменьшив смещение, что приведет к сбалансированному сочетанию недооценки и переоценки. Это легко выполнить путем бинарного поиска оптимального смещения, которое использует все числа с плавающей запятой в интервале [1, 2 n) в качестве тестовых примеров. Поступая таким образом, мы находим:

rootn (x, 2) ~= __int_as_float (0x1fbb4f2e + __float_as_int(x)/2) // max rel err = 3.47474e-2
rootn (x, 3) ~= __int_as_float (0x2a51067f + __float_as_int(x)/3) // max rel err = 3.15547e-2
rootn (x,-1) ~= __int_as_float (0x7ef311c2 - __float_as_int(x)/1) // max rel err = 5.05103e-2
rootn (x,-2) ~= __int_as_float (0x5f37642f - __float_as_int(x)/2) // max rel err = 3.42128e-2
rootn (x,-3) ~= __int_as_float (0x54a232a3 - __float_as_int(x)/3) // max rel err = 3.42405e-2

Некоторые могут заметить, что вычисление для rootn (x,-2) в основном является начальной частью Quake fast inverse square root.

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

Однако мне интересно, можно ли определить оптимальное смещение некоторой формулой замкнутой формы, так что максимальная абсолютная величина относительной ошибки max (| (approx (x, n) - x 1/n)/x 1/n |), минимизируется для всех x в [1,2 n). Для удобства изложения мы можем ограничить номерами binary32 (IEEE-754 с одной точностью).

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

Ответ 1

Здесь некоторый код Octave (MATLAB), который вычисляет смещения для положительного n, предполагая приведенные ниже гипотезы. Кажется, работает на 2 и 3, но я подозреваю, что одно из предположений ломается, когда n слишком велико. Нет времени для расследования прямо сейчас.

% finds the best offset for positive n
% assuming that the conjectures below are true
function oint = offset(n)
% find the worst upward error with no offset
efrac = (1 / log(2) - 1) * n;
evec = [floor(efrac) ceil(efrac)];
rootnvec = 2 .^ (evec / n);
[abserr i] = max((1 + evec / n) ./ rootnvec);
relerr = abserr - 1;
rootnx = rootnvec(i);
% conjecture: z such that approx(z, n) = 1
% should have the worst downward error
fun = @(o) relerr - o / rootnx + (1 / (1 + o * n) ^ (1 / n) - 1);
oreal = fzero(fun, [0 1]);
oint = round((127 * (1 - 1 / n) - oreal) * 2 ^ 23);

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

Пусть определим идеализированный вариант аппроксимации для x in [1, 2^n).

rootn-A(x, n) = 1 + floor(lg(x))/n + ((x/2^floor(lg(x))) - 1) / n
                    ^^^^^^^^^^^^^^   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                    contribution of       contribution of the
                     the exponent             significand

Мы хотим максимизировать rootn-A(x, n) / x^(1/n).

Кажется экспериментально, что максимум возникает, когда x является степенью двух. В этом случае значимый член равен нулю и floor(lg(x)) = lg(x), поэтому мы можем максимизировать

(1 + lg(x)/n) / x^(1/n).

Замените y = lg(x)/n, и мы можем максимизировать (1 + y) / 2^y для y in [0, 1) так, чтобы n*y было целым числом. Изменяя условие целостности, это упражнение исчисления, показывающее, что максимальная эта вогнутая функция находится в y = 1/log(2) - 1, около 0.4426950408889634. Отсюда следует, что максимум для x степени двух равен либо x = 2^floor((1/log(2) - 1) * n), либо x = 2^ceil((1/log(2) - 1) * n). Я предполагаю, что один из них на самом деле является глобальным максимумом.

В конце недооценки нам кажется, что мы хотим x, чтобы выход rootn(x, n) был 1, по крайней мере, для небольшого n. Позднее, надеюсь.