Как вы вычисляете основание журнала 2 в Java для целых чисел?

139

Я использую следующую функцию для вычисления логарифмической базы 2 для целых чисел:

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

Оптимальная производительность?

Кто-нибудь знает готовую для этого функцию J2SE API?

UPD1 На мой взгляд , арифметика с плавающей запятой оказалась быстрее, чем целочисленная арифметика.

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

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

Nulldevice
источник
1
Как вы проверяли работоспособность этого? В моей системе (Core i7, jdk 1.6 x64) целочисленная версия почти в 10 раз быстрее, чем версия с плавающей запятой. Убедитесь, что вы действительно что-то сделали с результатом функции, чтобы JIT не смогла полностью удалить расчет!
x4u
Ты прав. Я не использовал результаты расчета, а компилятор кое-что оптимизировал. Теперь у меня тот же результат, что и у вас - целочисленная функция в 10 раз быстрее (Core 2 Duo, jdk 1.6 c64)
Nulldevice
7
Это эффективно дает вам Math.floor(Math.log(n)/Math.log(2)), поэтому на самом деле это не вычисление логической базы 2!
Дори

Ответы:

74

Если вы думаете об использовании чисел с плавающей запятой для помощи с целочисленной арифметикой, будьте осторожны.

Обычно я стараюсь по возможности избегать вычислений FP.

Операции с плавающей запятой неточны. Никогда нельзя знать наверняка, что будет (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
...

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

Ротзор
источник
3
«это не значит, что он будет работать так же на любом ПК» - Было бы, если бы вы использовали strictfp, нет?
Ken
@Ken: Может быть ... Но в этом можно убедиться только после исчерпывающего перечисления всех возможных входных значений. (нам повезло, что их здесь так мало)
Rotsor
2
Технически да, но это верно для любой функции. В какой-то момент вы должны верить, что если вы воспользуетесь доступной документацией и протестируете какую-то хорошо подобранную, но исчезающе малую часть «всех возможных входных значений», ваша программа будет работать достаточно хорошо. strictfpкажется, на самом деле получил много дерьма за то, что был строгим. :-)
Ken
как насчет return ((long)Math.log(x) / (long)Math.log(base));решения всех ошибок?
Не ошибка
@Notabug не уверен в этом, но одним из побочных эффектов будет то, что ваш код будет работать некорректно для любых значений, которые не помещаются в long, это может быть бесполезно, если ваш диапазон значений превышает длинный диапазон (float имеет гораздо более высокий диапазон, чем long in java)
Naruto26
93

Это функция, которую я использую для этого расчета:

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 или новее реализация, подобная рассматриваемой, на самом деле немного быстрее, чем указанная 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-битная клиентская ВМ:

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

Серверная виртуальная машина 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 );
x4u
источник
9
BSRИнструкция x86 делает 32 - numberOfLeadingZeros, но не определена для 0, поэтому (JIT) компилятор должен проверять ненулевое значение, если он не может доказать, что это не обязательно. Введены расширения набора инструкций BMI (Haswell и новее) LZCNT, которые полностью реализуются numberOfLeadingZerosточно, в одной инструкции. Оба они имеют задержку в 3 цикла, по 1 на пропускную способность цикла. Поэтому я абсолютно рекомендую использовать numberOfLeadingZeros, потому что это упрощает создание хорошей JVM. (Одна странность в том, lzcntчто он имеет ложную зависимость от старого значения регистра, который он перезаписывает.)
Питер Кордес
Меня больше всего интересует ваш комментарий о замене встроенного процессора JIT-процессора Java 1.7. У вас есть ссылочный URL? (Ссылка на исходный код JIT тоже в порядке.)
kevinarpe
40

Пытаться Math.log(x) / Math.log(2)

Крис Б.
источник
9
Хотя математически это правильно, имейте в виду, что существует риск неправильного расчета из-за неточной арифметики с плавающей запятой, как объясняется в ответе Ротсора.
leeyuiwah
29

вы можете использовать личность

            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

hvgotcodes
источник
4
Хотя математически это правильно, имейте в виду, что существует риск неправильного расчета из-за неточной арифметики с плавающей запятой, как объясняется в ответе Ротсора.
leeyuiwah
20

Почему нет:

public static double log2(int n)
{
    return (Math.log(n) / Math.log(2));
}
Тофу пиво
источник
7
Хотя математически это правильно, имейте в виду, что существует риск неправильного расчета из-за неточной арифметики с плавающей запятой, как объясняется в ответе Ротсора.
leeyuiwah
10

В библиотеках гуавы есть функция:

LongMath.log2()

Поэтому я предлагаю его использовать.

Деметр
источник
Как я могу добавить этот пакет в свое приложение?
Эльвин Мамедов
Загрузите jar- файл отсюда и добавьте его в путь сборки вашего проекта.
Debosmit Ray
3
Должен ли я добавить библиотеку в свое приложение только для использования одной функции?
Таш Пемхива, 04
7
Почему именно вы предлагаете его использовать? Беглое прочтение исходного кода Guava показывает, что он делает то же самое, что и метод OP (несколько очень четко понимаемых строк кода) за счет добавления бесполезной зависимости. То, что Google что-то предлагает, не делает ничего лучше, чем понимание проблемы и ее решение самостоятельно.
Дэйв
4

Некоторые случаи просто работали, когда я использовал Math.log10:

public static double log2(int n)
{
    return (Math.log10(n) / Math.log10(2));
}
Марина
источник
3

Чтобы добавить к ответу x4u, который дает вам пол двоичного журнала числа, эта функция возвращает ceil двоичного журнала числа:

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 03
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
Этот ответ уже публиковался несколько раз и уже был замечен как потенциально неточный из-за ошибки округления. Обратите внимание, что OP запрашивает интегральное значение; совсем не ясно, какая точность округления должна использоваться, чтобы получить целое число.
AnotherParker