Як обчислити базу журналів 2 на Java для цілих чисел?


138

Я використовую наступну функцію для обчислення бази журналів 2 для цілих чисел:

public static int log2(int n){
    if(n <= 0) throw new IllegalArgumentException();
    return 31 - Integer.numberOfLeadingZeros(n);
}

Чи має він оптимальні показники?

Хтось знає готову функцію API J2SE для цієї мети?

UPD1 Дивно, для мене арифметика з плаваючою точкою виявляється швидшою, ніж арифметика з цілими числами.

UPD2 Завдяки коментарям я проведу більш детальне розслідування.

UPD3 Моя ціла арифметична функція в 10 разів швидша, ніж Math.log (n) /Math.log (2).


1
Як ви перевірили ефективність цього? У моїй системі (Core i7, jdk 1.6 x64) ціла версія майже в 10 разів швидша, ніж версія з плаваючою комою. Будьте впевнені, що насправді щось роблять з результатом функції, щоб JIT не міг повністю видалити обчислення!
x4u

Ви праві. Я не використовував результати обчислення, і компілятор щось оптимізував. Тепер у мене такий же результат, як і ви - ціла функція в 10 разів швидша (Core 2 Duo, jdk 1.6
c64

6
Це ефективно дає вам Math.floor(Math.log(n)/Math.log(2)), так що це не дуже обчислювальна база журналів 2!
Дорі

Відповіді:


74

Якщо ви думаєте про використання плаваючої точки для допомоги з цілою арифметикою, ви повинні бути обережними.

Зазвичай я намагаюся уникати розрахунків ПЗ, коли це можливо.

Операції з плаваючою комою не є точними. Ніколи не можеш точно знати, на що (int)(Math.log(65536)/Math.log(2))оцінить. Наприклад, Math.ceil(Math.log(1<<29) / Math.log(2))це 30 на моєму комп'ютері, де математично це повинно бути рівно 29. Я не знайшов значення для x, де (int)(Math.log(x)/Math.log(2))виходить з ладу (тільки тому, що є лише 32 "небезпечні" значення), але це не означає, що він буде працювати таким же чином на будь-якому ПК.

Звичайний трюк тут - використання "епсілона" при округленні. Як (int)(Math.log(x)/Math.log(2)+1e-10)ніколи не слід провалюватися. Вибір цього "епсилона" - не тривіальне завдання.

Більше демонстрації, використання більш загального завдання - спроба реалізації int log(int x, int base):

Код тестування:

static int pow(int base, int power) {
    int result = 1;
    for (int i = 0; i < power; i++)
        result *= base;
    return result;
}

private static void test(int base, int pow) {
    int x = pow(base, pow);
    if (pow != log(x, base))
        System.out.println(String.format("error at %d^%d", base, pow));
    if(pow!=0 && (pow-1) != log(x-1, base))
        System.out.println(String.format("error at %d^%d-1", base, pow));
}

public static void main(String[] args) {
    for (int base = 2; base < 500; base++) {
        int maxPow = (int) (Math.log(Integer.MAX_VALUE) / Math.log(base));
        for (int pow = 0; pow <= maxPow; pow++) {
            test(base, pow);
        }
    }
}

Якщо ми використовуємо найбільш пряму реалізацію логарифму,

static int log(int x, int base)
{
    return (int) (Math.log(x) / Math.log(base));
}

це відбитки:

error at 3^5
error at 3^10
error at 3^13
error at 3^15
error at 3^17
error at 9^5
error at 10^3
error at 10^6
error at 10^9
error at 11^7
error at 12^7
...

Щоб повністю позбутися помилок, мені довелося додати епсилон, який знаходиться між 1е-11 та 1е-14. Чи могли б ви сказати це перед тестуванням? Я точно не міг.


3
"це не означає, що він буде працювати однаково на будь-якому ПК" - Це було б, якби ви використовували strictfp, ні?
Кен

@Ken: Можливо ... Але ви можете бути впевнені лише після того, як вичерпно перерахували всі можливі вхідні значення. (нам пощастило, що їх тут так мало)
Ротсор,

2
Технічно так, але це стосується будь-якої функції. У якийсь момент вам доведеться довіритися, що якщо ви використовуєте наявну документацію, і випробуєте якусь добре підібрану, але гарно малу частину "всіх можливих вхідних значень", що ваша програма буде працювати досить добре. strictfpздається, насправді отримав багато лайно за те, що насправді суворий. :-)
Кен

як щодо return ((long)Math.log(x) / (long)Math.log(base));вирішення всіх помилок?
Не помилка

92

Це функція, яку я використовую для цього обчислення:

public static int binlog( int bits ) // returns 0 for bits=0
{
    int log = 0;
    if( ( bits & 0xffff0000 ) != 0 ) { bits >>>= 16; log = 16; }
    if( bits >= 256 ) { bits >>>= 8; log += 8; }
    if( bits >= 16  ) { bits >>>= 4; log += 4; }
    if( bits >= 4   ) { bits >>>= 2; log += 2; }
    return log + ( bits >>> 1 );
}

Це трохи швидше, ніж Integer.numberOfLeadingZeros () (20-30%) і майже в 10 разів швидше (jdk 1.6 x64), ніж реалізація на основі Math.log (), як ця:

private static final double log2div = 1.000000000001 / Math.log( 2 );
public static int log2fp0( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return (int) ( Math.log( bits & 0xffffffffL ) * log2div );
}

Обидві функції повертають однакові результати для всіх можливих вхідних значень.

Оновлення: JIT-сервер Java 1.7 здатний замінити кілька статичних математичних функцій альтернативними реалізаціями на основі внутрішніх процесорних процесів. Однією з таких функцій є Integer.numberOfLeadingZeros (). Отже, з версією 1.7 або більш нової версії сервера VM, реалізація, подібна до питання, насправді трохи швидша, ніж binlogвище. На жаль, клієнт JIT, схоже, не має такої оптимізації.

public static int log2nlz( int bits )
{
    if( bits == 0 )
        return 0; // or throw exception
    return 31 - Integer.numberOfLeadingZeros( bits );
}

Ця реалізація також повертає однакові результати для всіх 2 ^ 32 можливих вхідних значень, як і інші дві реалізації, які я розмістив вище.

Ось фактичні умови виконання мого ПК (Sandy Bridge i7):

JDK 1.7 32 біт-клієнт VM:

binlog:         11.5s
log2nlz:        16.5s
log2fp:        118.1s
log(x)/log(2): 165.0s

VM сервера JDK 1.7 x64:

binlog:          5.8s
log2nlz:         5.1s
log2fp:         89.5s
log(x)/log(2): 108.1s

Це тестовий код:

int sum = 0, x = 0;
long time = System.nanoTime();
do sum += log2nlz( x ); while( ++x != 0 );
time = System.nanoTime() - time;
System.out.println( "time=" + time / 1000000L / 1000.0 + "s -> " + sum );

9
BSRІнструкція x86 робить 32 - numberOfLeadingZeros, але невизначена для 0, тому компілятор (JIT) повинен перевірити наявність ненульового значення, якщо він не може довести, що цього не потрібно. Введено розширення набору інструкцій BMI (Haswell та новіших) LZCNT, що повністю реалізує numberOfLeadingZerosточно, в одній інструкції. Вони обоє три затримки циклу, 1 за пропускну здатність циклу. Тому я б абсолютно рекомендував використовувати numberOfLeadingZeros, оскільки це полегшує хороший JVM. (Одне дивне в тому lzcnt, що він має помилкову залежність від старого значення регістра, який він перезаписує.)
Пітер Кордес

Мене найбільше цікавить ваш коментар щодо заміни внутрішніх технологічних процесорів Java 1.7 сервера JIT. У вас є довідкова URL-адреса? (Посилання з вихідним кодом JIT також добре.)
kevinarpe

37

Спробуйте Math.log(x) / Math.log(2)


8
Хоча математично це правильно, майте на увазі, що існує ризик неправильного обчислення через неточну арифметику з плаваючою комою, як це пояснено у відповіді Ротсора.
leeyuiwah

28

ви можете використовувати посвідчення

            log[a]x
 log[b]x = ---------
            log[a]b

тож це було б застосовано для log2.

            log[10]x
 log[2]x = ----------
            log[10]2

просто підключіть це до методу java Math log10 ....

http://mathforum.org/library/drmath/view/55565.html


3
Хоча математично це правильно, майте на увазі, що існує ризик неправильного обчислення через неточну арифметику з плаваючою комою, як це пояснено у відповіді Ротсора.
leeyuiwah

18

Чому ні:

public static double log2(int n)
{
    return (Math.log(n) / Math.log(2));
}

6
Хоча математично це правильно, майте на увазі, що існує ризик неправильного обчислення через неточну арифметику з плаваючою комою, як пояснено у відповіді Ротсора.
leeyuiwah

9

У бібліотеках guava є функція:

LongMath.log2()

Тому я пропоную використовувати його.


Як я можу додати цей пакет до своєї програми?
Ельвін Мамедов

Завантажте банку звідси і додайте її до контуру збирання вашого проекту.
Дебосміт Рей

2
Чи слід додати бібліотеку до своєї програми лише для використання однієї функції?
Таш Пемхіва

7
Чому саме ви б запропонували його використовувати? Швидке читання джерела Guava показує, що він робить те саме, що і метод ОП (кілька дуже чітко зрозумілих рядків коду), ціною додавання інакше марної залежності. Тільки тому, що Google надає щось, це не є кращим, ніж зрозуміти проблему та вирішити самостійно.
Дейв

3

Щоб додати відповідь x4u, яка дає вам підлогу двійкового журналу числа, ця функція повертає стелю двійкового журналу числа:

public static int ceilbinlog(int number) // returns 0 for bits=0
{
    int log = 0;
    int bits = number;
    if ((bits & 0xffff0000) != 0) {
        bits >>>= 16;
        log = 16;
    }
    if (bits >= 256) {
        bits >>>= 8;
        log += 8;
    }
    if (bits >= 16) {
        bits >>>= 4;
        log += 4;
    }
    if (bits >= 4) {
        bits >>>= 2;
        log += 2;
    }
    if (1 << log < number)
        log++;
    return log + (bits >>> 1);
}

Де змінна "число"?
barteks2x

3

Деякі випадки працювали, коли я використовував Math.log10:

public static double log2(int n)
{
    return (Math.log10(n) / Math.log10(2));
}

0

додамо:

int[] fastLogs;

private void populateFastLogs(int length) {
    fastLogs = new int[length + 1];
    int counter = 0;
    int log = 0;
    int num = 1;
    fastLogs[0] = 0;
    for (int i = 1; i < fastLogs.length; i++) {
        counter++;
        fastLogs[i] = log;
        if (counter == num) {
            log++;
            num *= 2;
            counter = 0;
        }
    }
}

Джерело: https://github.com/pochuan/cs166/blob/master/ps1/rmq/SparseTableRMQ.java


Це створило б таблицю пошуку. ОП попросила більш швидкий спосіб "обчислити" логарифм.
Дейв

-4

Для обчислення бази журналів 2 з n може бути використаний наступний вираз:

double res = log10(n)/log10(2);

2
Ця відповідь уже опублікована кілька разів, і її вже було помічено як потенційно неточну через помилку округлення. Зверніть увагу, що ОП запитували про цілісне значення; зовсім не ясно, яку точність округлення потрібно використовувати, щоб дістатися звідси до цілого числа.
ЩеПаркер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.