Быстрая фиксированная точка pow, log, exp и sqrt
У меня есть класс с фиксированной точкой (10.22), и мне нужны pow, sqrt, exp и функция журнала.
увы, я понятия не имею, с чего начать. Может ли кто-нибудь предоставить мне ссылки на полезные статьи или, еще лучше, предоставить мне какой-то код?
Я предполагаю, что как только у меня есть функция exp, становится относительно легко реализовать pow и sqrt, как они только что стали.
pow (x, y ) => exp( y * log( x ) ) sqrt (x ) => pow( x, 0.5)
Это только те функции exp и log, которые я нахожу трудными (как будто я помню несколько моих правил журнала, я не могу вспомнить о них больше).
предположительно, кстати, также будет более быстрый метод для sqrt и pow, поэтому любые указатели на этом фронте будут оценены, даже если его просто сказать, используйте методы, которые я описываю выше:)
обратите внимание: это должна быть кросс-платформа и в чистом коде C / C++, поэтому я не могу использовать ассемблер оптимизации.
4 ответов
очень простое решение-использовать приличное табличное приближение. На самом деле вам не нужно много данных, если вы правильно уменьшаете входные данные. exp(a)==exp(a/2)*exp(a/2)
, Что означает, что вам действительно нужно только рассчитать exp(x)
на 1 < x < 2
. В этом диапазоне приближение Рунга-Кутты даст разумные результаты с ~16 записями IIRC.
аналогично, sqrt(a) == 2 * sqrt(a/4)
что означает, что вам нужны только записи таблицы для 1 < a < 4
. Log (a) немного сложнее: log(a) == 1 + log(a/e)
. Это довольно медленная итерация, но log (1024) - это только 6.9, поэтому у вас не будет много итераций.
вы бы использовали аналогичный алгоритм" integer-first " для pow:pow(x,y)==pow(x, floor(y)) * pow(x, frac(y))
. Это работает, потому что pow(double, int)
тривиально (разделяй и властвуй).
[edit] для интегральной компоненты log(a)
, может быть полезно хранить таблицу 1, e, e^2, e^3, e^4, e^5, e^6, e^7
таким образом, вы можете уменьшить log(a) == n + log(a/e^n)
простым жестко закодированным двоичным поиском a в этой таблице. Улучшение от 7 до 3 шагов не так велико, но это означает, что вам нужно разделить только один раз на e^n
вместо n
раз e
.
[редактирование 2]
И за это последнее log(a/e^n)
перспективе, вы можете использовать log(a/e^n) = log((a/e^n)^8)/8
- каждая итерация создает еще 3 бита таблица подстановки. Это держит ваш код и размер таблицы небольшими. Это обычно код для встроенных систем, и у них нет больших кэшей.
[редактирование 3]
Это все равно не умно с моей стороны. log(a) = log(2) + log(a/2)
. Вы можете просто сохранить значение фиксированной точки log2=0.30102999566
, подсчитайте количество ведущих нулей, сдвиг a
в диапазон, используемый для вашей таблицы поиска, и умножьте этот сдвиг (целое число) на константу с фиксированной точкой log2
. Может быть как низко как 3 Инструкции.
используя e
для шага сокращения просто дает вам "хороший"log(e)=1.0
константа, но это ложная оптимизация. 0,30102999566-такая же хорошая константа, как 1.0; оба являются 32-битными константами в фиксированной точке 10.22. Использование 2 в качестве константы для уменьшения диапазона позволяет использовать битовый сдвиг для деления.
вы все еще получаете трюк от edit 2,log(a/2^n) = log((a/2^n)^8)/8
. В принципе, это дает вам результат (a + b/8 + c/64 + d/512) * 0.30102999566
- С b,c, d в диапазоне [0,7]. a.bcd
действительно восьмеричное число. Не удивительно, так как мы использовали 8 как власть. (Трюк одинаково хорошо работает с мощностью 2, 4 или 16.)
[редактирование 4]
Все еще был открытый конец. pow(x, frac(y)
это просто pow(sqrt(x), 2 * frac(y))
а у нас приличный 1/sqrt(x)
. Это дает нам гораздо более эффективный подход. Скажи frac(y)=0.101
бинарный, т. е. 1/2 плюс 1/8. Тогда это значит x^0.101
is (x^1/2 * x^1/8)
. Но!--31--> это просто sqrt(x)
и x^1/8
is (sqrt(sqrt(sqrt(x)))
. Экономя еще одну операцию, Ньютон-Рафсон!--35--> дает 1/sqrt(x)
так мы вычисляем 1.0/(NR(x)*NR((NR(NR(x)))
. Мы только инвертируем конечный результат, не используем функцию sqrt напрямую.
ниже приведен пример реализации C алгоритма 2 фиксированной точки журнала Clay S. Turner[1]. Алгоритм не требует какой-либо таблице. Это может быть полезно в системах, где ограничения памяти жесткие, а процессор не имеет FPU, как в случае со многими микроконтроллерами. База журналов e и база журналов 10 также поддерживаются с помощью свойства логарифмов, которые для любой базы n:
log (x)
y
log (x) = _______
n log (n)
y
где, для этого алгоритма,y равна 2.
приятной особенностью этой реализации является то, что она поддерживает переменную точность: точность может быть определена во время выполнения за счет диапазона. Как я это реализовал, процессор (или компилятор) должен быть способен выполнять 64-битную математику для хранения некоторых промежуточных результатов. Его можно легко приспособиться для того чтобы не требовать 64-разрядной поддержки, но ряд будет уменьшенный.
при использовании этих функций x
ожидается, что значение масштабируется в соответствии с
указано precision
. Например, если precision
будет 16, тогда x
должен быть увеличен на 2^16 (65536). Результатом является значение фиксированной точки с тем же масштабным коэффициентом, что и вход. Возвращаемое значение INT32_MIN
является отрицательная бесконечность. Возвращаемое значение INT32_MAX
указывает на ошибку и errno
будет установлен в EINVAL
, указывая, что точность ввода была недействительный.
#include <errno.h>
#include <stddef.h>
#include "log2fix.h"
#define INV_LOG2_E_Q1DOT31 UINT64_C(0x58b90bfc) // Inverse log base 2 of e
#define INV_LOG2_10_Q1DOT31 UINT64_C(0x268826a1) // Inverse log base 2 of 10
int32_t log2fix (uint32_t x, size_t precision)
{
int32_t b = 1U << (precision - 1);
int32_t y = 0;
if (precision < 1 || precision > 31) {
errno = EINVAL;
return INT32_MAX; // indicates an error
}
if (x == 0) {
return INT32_MIN; // represents negative infinity
}
while (x < 1U << precision) {
x <<= 1;
y -= 1U << precision;
}
while (x >= 2U << precision) {
x >>= 1;
y += 1U << precision;
}
uint64_t z = x;
for (size_t i = 0; i < precision; i++) {
z = z * z >> precision;
if (z >= 2U << precision) {
z >>= 1;
y += b;
}
b >>= 1;
}
return y;
}
int32_t logfix (uint32_t x, size_t precision)
{
uint64_t t;
t = log2fix(x, precision) * INV_LOG2_E_Q1DOT31;
return t >> 31;
}
int32_t log10fix (uint32_t x, size_t precision)
{
uint64_t t;
t = log2fix(x, precision) * INV_LOG2_10_Q1DOT31;
return t >> 31;
}
код для этой реализации также живет в Github, а также пример / тестовая программа, которая иллюстрирует, как использовать эту функцию для вычисления и отображения логарифмов из чисел, считанных со стандартного ввода.
[1] S. С. Тернер, "Быстрый Алгоритм Двоичного Логарифма", IEEE обработки сигналов Mag., PP. 124,140, Sep. 2010.
хорошей отправной точкой является книга Джека Креншоу, "математический инструментарий для программирования в реальном времени". Он имеет хорошее обсуждение алгоритмов и реализаций для различных трансцендентальных функций.
Проверьте мою реализацию sqrt с фиксированной точкой, используя только целочисленные операции. Это было весело придумывать. Уже совсем старый.
в противном случае проверяем CORDIC набор алгоритмов. Это способ реализовать все перечисленные функции и тригонометрические функции.
EDIT: I опубликовал рецензируемый источник на GitHub здесь