Как максимально точно вычислить log2 целого числа в C с помощью побитовых операций
Мне нужно рассчитать энтропию, и из-за ограничений моей системы мне нужно использовать ограниченные функции C (без циклов, без поддержки с плавающей запятой) и мне нужна как можно большая точность. Отсюда я выясняю, как оценить пол log2 целого числа, используя побитовые операции. Тем не менее, мне нужно повысить точность результатов. Поскольку операции с плавающей запятой не допускаются, есть ли способ рассчитать log2(x/y)
с x < y
так что результат будет что-то вроде log2(x/y)*10000
, чтобы получить нужную мне точность с помощью арифметического целого числа?
1 ответ
Вы будете основывать алгоритм по формуле
log2(x/y) = K*(-log(x/y));
где
K = -1.0/log(2.0); // you can precompute this constant before run-time
a = (y-x)/y;
-log(x/y) = a + a^2/2 + a^3/3 + a^4/4 + a^5/5 + ...
Если вы пишете цикл правильно - или, если хотите, разверните цикл для кодирования той же последовательности операций без цикла - тогда вы можете обрабатывать все в целочисленных операциях:
(y^N*(1*2*3*4*5*...*N)) * (-log(x/y))
= y^(N-1)*(2*3*4*5*...*N)*(y-x) + y^(N-2)*(1*3*4*5*...*N)*(y-x)^2 + ...
Конечно, ^
, силовой оператор, крепче *
, не является оператором C, но вы можете эффективно реализовать это в контексте вашего (возможно, развернутого) цикла как работающий продукт.
N
это целое число, достаточно большое, чтобы обеспечить желаемую точность, но не настолько большое, чтобы оно превышало количество доступных битов. Если не уверены, то попробуйте N = 6
например. относительно K
вы можете возразить, что это число с плавающей запятой, но это не проблема для вас, потому что вы собираетесь предварительно вычислить K
, храня его как соотношение целых чисел.
ОБРАЗЕЦ КОДА
Это игрушечный код, но он работает для небольших значений x
а также y
такие как 5 и 7, таким образом, достаточно, чтобы доказать концепцию. В игрушечном коде большие значения могут молча переполнять стандартные 64-битные регистры. Чтобы сделать код устойчивым, потребуется больше работы.
#include <stddef.h>
#include <stdlib.h>
// Your program will not need the below headers, which are here
// included only for comparison and demonstration.
#include <math.h>
#include <stdio.h>
const size_t N = 6;
const long long Ky = 1 << 10; // denominator of K
// Your code should define a precomputed value for Kx here.
int main(const int argc, const char *const *const argv)
{
// Your program won't include the following library calls but this
// does not matter. You can instead precompute the value of Kx and
// hard-code its value above with Ky.
const long long Kx = lrintl((-1.0/log(2.0))*Ky); // numerator of K
printf("K == %lld/%lld\n", Kx, Ky);
if (argc != 3) exit(1);
// Read x and y from the command line.
const long long x0 = atoll(argv[1]);
const long long y = atoll(argv[2]);
printf("x/y == %lld/%lld\n", x0, y);
if (x0 <= 0 || y <= 0 || x0 > y) exit(1);
// If 2*x <= y, then, to improve accuracy, double x repeatedly
// until 2*x > y. Each doubling offsets the log2 by 1. The offset
// is to be recovered later.
long long x = x0;
int integral_part_of_log2 = 0;
while (1) {
const long long trial_x = x << 1;
if (trial_x > y) break;
x = trial_x;
--integral_part_of_log2;
}
printf("integral_part_of_log2 == %d\n", integral_part_of_log2);
// Calculate the denominator of -log(x/y).
long long yy = 1;
for (size_t j = N; j; --j) yy *= j*y;
// Calculate the numerator of -log(x/y).
long long xx = 0;
{
const long long y_minus_x = y - x;
for (size_t i = N; i; --i) {
long long term = 1;
size_t j = N;
for (; j > i; --j) {
term *= j*y;
}
term *= y_minus_x;
--j;
for (; j; --j) {
term *= j*y_minus_x;
}
xx += term;
}
}
// Convert log to log2.
xx *= Kx;
yy *= Ky;
// Restore the aforementioned offset.
for (; integral_part_of_log2; ++integral_part_of_log2) xx -= yy;
printf("log2(%lld/%lld) == %lld/%lld\n", x0, y, xx, yy);
printf("in floating point, this ratio of integers works out to %g\n",
(1.0*xx)/(1.0*yy));
printf("the CPU's floating-point unit computes the log2 to be %g\n",
log2((1.0*x0)/(1.0*y)));
return 0;
}
Запуск этого на моей машине с аргументами командной строки 5 7
, это выводит:
K == -1477/1024
x/y == 5/7
integral_part_of_log2 == 0
log2(5/7) == -42093223872/86740254720
in floating point, this ratio of integers works out to -0.485279
the CPU's floating-point unit computes the log2 to be -0.485427
Точность была бы существенно улучшена N = 12
а также Ky = 1 << 20
, но для этого вам нужен либо более экономичный код, либо более 64 бит.
ТРИФТЕРНЫЙ КОД
Более сложный код, требующий больше усилий для написания, может представлять числитель и знаменатель в простых факторах. Например, он может представлять 500 как [2 0 3], что означает (22) (30) (53).
Еще больше улучшений может произойти с вашим воображением.
АЛЬТЕРНАТИВНЫЙ ПОДХОД
Для альтернативного подхода, хотя он может не соответствовать вашим требованиям точно так, как вы их сформулировали, @phuclv дал предложение, которому я бы склонен следовать, если бы ваша программа была моей: решите проблему в обратном порядке, угадав значение c/d
для логарифма, а затем вычисления 2^(c/d)
предположительно через итерацию Ньютона-Рафсона. Лично мне больше нравится подход Ньютона-Рафсона. См. Раздел 4.8 здесь (мой оригинал).
МАТЕМАТИЧЕСКИЙ ФОН
Несколько источников, включая мой, уже связаны, объясняют ряд Тейлора, лежащий в основе первого подхода, и итерацию Ньютона-Рафсона второго подхода. Математика, к сожалению, нетривиальна, но она у вас есть. Удачи.