Обратная гиперболическая функция asinh()
тесно связана с натуральным логарифмом. Я пытаюсь определить наиболее точный способ вычисления asinh()
из стандартной математической функции C99 log1p()
. Для удобства экспериментов я ограничиваю себя вычислением одноточечной обработки IEEE-754 прямо сейчас, то есть смотрю asinhf()
и log1pf()
. Я намерен повторно использовать тот же самый алгоритм для вычисления двойной точности, то есть asinh()
и log1p()
, позже.Самый точный способ вычисления asinhf() из log1pf()?
Моя основная цель - минимизировать ошибку ulp, вторичная цель - свести к минимуму количество неверно округленных результатов, при условии, что улучшенный код будет минимально медленнее, чем версии, указанные ниже. Любое постепенное улучшение точности, скажем 0,2 ulp, было бы очень желанным. Добавление нескольких FMA (fused multiply-add) было бы неплохо, с другой стороны, я надеюсь, кто-то сможет определить решение, в котором используется быстрый rsqrtf()
(обратный квадратный корень).
Полученный код C99 должен поддаваться векторизации, возможно, некоторыми незначительными прямыми преобразованиями. Все промежуточные вычисления должны соответствовать с точностью до аргумента функции и результата, так как любой переключатель с более высокой точностью может оказать серьезное отрицательное влияние на производительность. Код должен корректно работать как с поддержкой денормальной поддержки IEEE-754, так и в режиме FTZ (с нуля до нуля).
До сих пор я идентифицировал следующие две реализации кандидата. Обратите внимание, что код может быть легко преобразован в безветровую векторную версию с одним вызовом до log1pf()
, но я не сделал этого на этом этапе, чтобы избежать ненужной запутывания.
/* for a >= 0, asinh(a) = log (a + sqrt (a*a+1))
= log1p (a + (sqrt (a*a+1) - 1))
= log1p (a + sqrt1pm1 (a*a))
= log1p (a + (a*a/(1 + sqrt(a*a + 1))))
= log1p (a + a * (a/(1 + sqrt(a*a + 1))))
= log1p (fma (a/(1 + sqrt(a*a + 1)), a, a)
= log1p (fma (1/(1/a + sqrt(1/a*a + 1)), a, a)
*/
float my_asinhf (float a)
{
float fa, t;
fa = fabsf (a);
#if !USE_RECIPROCAL
if (fa >= 0x1.0p64f) { // prevent overflow in intermediate computation
t = log1pf (fa) + 0x1.62e430p-1f; // log(2)
} else {
t = fmaf (fa/(1.0f + sqrtf (fmaf (fa, fa, 1.0f))), fa, fa);
t = log1pf (t);
}
#else // USE_RECIPROCAL
if (fa > 0x1.0p126f) { // prevent underflow in intermediate computation
t = log1pf (fa) + 0x1.62e430p-1f; // log(2)
} else {
t = 1.0f/fa;
t = fmaf (1.0f/(t + sqrtf (fmaf (t, t, 1.0f))), fa, fa);
t = log1pf (t);
}
#endif // USE_RECIPROCAL
return copysignf (t, a); // restore sign
}
С конкретной log1pf()
реализации, который с точностью до 0,6 < ulps, я наблюдаю следующую статистику ошибок при тестировании исчерпывающе во всех 2 возможных IEEE-754 с одинарной точностью входов. Когда USE_RECIPROCAL = 0
, максимальная ошибка равна 1.49486 ulp, и есть 353 587 822 неверно округленные результаты. С USE_RECIPROCAL = 1
максимальная ошибка 1.50805 ulp, и только 77 569 390 неверно округленных результатов.
С точки зрения производительности, вариант USE_RECIPROCAL = 0
будет быстрее, если обратные и полные деления будут занимать примерно одинаковое количество времени, но вариант USE_RECIPROCAL = 1
может быть быстрее, если доступна очень быстрая взаимная поддержка. Ответы могут предполагать, что все основные арифметические операции, в том числе FMA (плавное умножение), правильно округлены в соответствии с режимом «круглый-самый-четный» IEEE-754. Кроме того,, более быстрая, почти правильно закругленная, версии взаимных и rsqrtf()
могут быть доступны, где «почти правильно округленная» означает, что максимальная ошибка ulp будет ограничена примерно 0,53 ulps и подавляющим большинством результатов, скажем, 95%, правильно округлены. Базовая арифметика с направленными закруглениями может быть доступной без каких-либо дополнительных затрат.
Если вам нужна точность, точьте 'float' и перейдите в' double' asap или даже 'long double', если ваш компилятор поддерживает представление на 80-битных чисел. –
Это не реалистичный вариант по соображениям производительности, все промежуточные вычисления должны выполняться с «собственной точностью» ввода и результата. Я отредактирую это требование в вопросе. – njuffa
'double' является более родным, чем' float', который теперь подходит только для некоторых очень ограниченных систем, таких как встроенные. –