Эффективная реализация натурального логарифма (ln) и возведения в степень - PullRequest
9 голосов
/ 21 марта 2012

В основном я ищу реализацию функций log() и exp(), предоставляемых в библиотеке C <math.h>.Я работаю с 8-битными микроконтроллерами (OKI 411 и 431).Мне нужно рассчитать Средняя кинетическая температура .Требование состоит в том, что мы должны иметь возможность вычислять MKT как можно быстрее и с минимальным объемом памяти кода.Компилятор поставляется с функциями log() и exp() в <math.h>.Но вызов любой функции и связывание с библиотекой приводит к увеличению размера кода на 5 килобайт, что не помещается ни в один из микроуровней, с которыми мы работаем (OKI 411), поскольку наш код уже потребил ~ 12 КБ доступной ~ 15 КБ памяти кода.

Реализация, которую я ищу, не должна использовать какие-либо другие функции библиотеки C (например, pow (), sqrt () и т. Д.).Это связано с тем, что все библиотечные функции упакованы в одну библиотеку, и даже если вызывается одна функция, компоновщик перенесет целую библиотеку 5K в память кода.

EDIT

Алгоритм должен быть корректным до 3десятичные разряды.

Ответы [ 5 ]

11 голосов
/ 29 мая 2017

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

Для 3 цифр точности выполните следующие команды в Maple:

with(numapprox):
Digits := 8
minimax(ln(x), x = 1 .. 2, 4, 1, 'maxerror')
maxerror

Его ответ следующий полином:

-1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x

С максимальной погрешностью: 0,000061011436

Remez approximation of a function

Мы сгенерировали полином, который приближает ln (x), но только внутри интервала [1..2]. Увеличение интервала не является разумным, потому что это увеличит максимальную ошибку еще больше. Вместо этого сделайте следующее разложение:

formula

Итак, сначала найдите наибольшую степень 2, которая по-прежнему меньше, чем число (см .: Какой самый быстрый / самый эффективный способ найти старший установленный бит (мсб) в целом числе в C? ). Это число на самом деле является логарифмом основания-2. Разделите это значение, и результат попадет в интервал 1..2. В конце нам нужно будет добавить n * ln (2), чтобы получить окончательный результат.

Пример реализации для чисел> = 1:

float ln(float y) {
    int log2;
    float divisor, x, result;

    log2 = msb((int)y); // See: https://stackoverflow.com/a/4970859/6630230
    divisor = (float)(1 << log2);
    x = y / divisor;    // normalized value between [1.0, 2.0]

    result = -1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x;
    result += ((float)log2) * 0.69314718; // ln(2) = 0.69314718

    return result;
}

Хотя, если вы планируете использовать его только в интервале [1.0, 2.0], функция выглядит так:

float ln(float x) {
    return -1.7417939 + (2.8212026 + (-1.4699568 + (0.44717955 - 0.056570851 * x) * x) * x) * x;
}
8 голосов
/ 21 марта 2012

Серия Тейлора для e ^ x чрезвычайно быстро сходится, и вы можете настроить свою реализацию с необходимой вам точностью.(http://en.wikipedia.org/wiki/Taylor_series)

Серия Тейлора для журнала не так хороша ...

5 голосов
/ 27 августа 2013

Если вам не нужна математика с плавающей точкой для чего-либо еще, вы можете довольно легко вычислить приблизительный дробный логарифм с базой 2.Начните со смещения значения влево до 32768 или выше и сохраните количество раз, которое вы сделали, в count.Затем повторите некоторое количество раз (в зависимости от желаемого масштабного коэффициента):

n = (mult(n,n) + 32768u) >> 16; // If a function is available for 16x16->32 multiply
count<<=1;
if (n < 32768) n*=2; else count+=1;

Если вышеуказанный цикл повторяется 8 раз, то логарифмическая основа 2 этого числа будет считать / 256.Если десять раз, посчитай / 1024.Если одиннадцать, считать / 2048.По сути, эта функция работает путем вычисления целочисленного логарифма степени двух n ** (2 ^ повторения), но с промежуточными значениями, масштабируемыми во избежание переполнения.

5 голосов
/ 21 марта 2012

Будет ли работать базовая таблица с интерполяцией между значениями? Если диапазоны значений ограничены (что вероятно для вашего случая - я сомневаюсь, что показания температуры имеют огромный диапазон) и высокая точность не требуется, это может сработать. Должно быть легко проверить на обычной машине.

Вот одна из многих тем о табличном представлении функций: Вычисление и просмотр таблиц для получения значения синуса?

1 голос
/ 03 апреля 2018

В дополнение к ответу Краучинга Котенка, который вдохновил меня, вы можете построить псевдорекурсивный (не более 1 самозвонка) логарифм, чтобы избежать использования полиномов. В псевдокоде

ln(x) :=
If (x <= 0)
    return NaN
Else if (!(1 <= x < 2))
    return LN2 * b + ln(a)
Else
    return taylor_expansion(x - 1)

Это довольно эффективно и точно, поскольку на [1; 2) ряд Тейлора сходится НАМНОГО быстрее, и мы получаем такое число 1 <= a <2 с первым вызовом ln, если наш вход положительный, но не в этом диапазоне. </p>

Вы можете найти 'b' как ваш беспристрастный показатель по данным, хранящимся в поплавке x, и 'a' из мантиссы с плавающей запятой x (a - это то же самое, что и x, но теперь с показателем biased_0, а не показатель biased_b). LN2 следует хранить как макрос в шестнадцатеричном формате с плавающей запятой IMO. Вы также можете использовать http://man7.org/linux/man-pages/man3/frexp.3.html для этого.

Также хитрость

unsigned long tmp = *(ulong*)(&d);

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

...