¿Cómo se calcula la base de registro 2 en Java para enteros?


138

Utilizo la siguiente función para calcular la base de registro 2 para enteros:

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

¿Tiene un rendimiento óptimo?

¿Alguien sabe la función API J2SE lista para ese propósito?

UPD1 Sorprendentemente para mí, la aritmética de coma flotante parece ser más rápida que la aritmética de enteros.

UPD2 Debido a los comentarios, realizaré una investigación más detallada.

UPD3 Mi función aritmética de enteros es 10 veces más rápida que Math.log (n) /Math.log (2).


1
¿Cómo probaste el rendimiento de esto? En mi sistema (Core i7, jdk 1.6 x64) la versión entera es casi 10 veces más rápida que la versión de punto flotante. ¡Asegúrese de hacer algo con el resultado de la función para que el JIT no pueda eliminar el cálculo por completo!
x4u

Estás en lo correcto. No utilicé resultados de cálculo y el compilador ha optimizado algo. Ahora tengo el mismo resultado que usted: la función entera es 10 veces más rápida (Core 2 Duo, jdk 1.6 c64)
Nulldevice

66
Esto efectivamente te da Math.floor(Math.log(n)/Math.log(2)), ¡así que no está realmente calculando la base de registro 2!
Dori

Respuestas:


74

Si está pensando en usar coma flotante para ayudar con la aritmética de enteros, debe tener cuidado.

Por lo general, trato de evitar los cálculos de FP siempre que sea posible.

Las operaciones de punto flotante no son exactas. Nunca se puede saber con certeza qué (int)(Math.log(65536)/Math.log(2))evaluará. Por ejemplo, Math.ceil(Math.log(1<<29) / Math.log(2))es 30 en mi PC donde matemáticamente debería ser exactamente 29. No encontré un valor para x donde (int)(Math.log(x)/Math.log(2))falla (solo porque solo hay 32 valores "peligrosos"), pero eso no significa que funcionará de la misma manera en cualquier PC.

El truco habitual aquí es usar "epsilon" al redondear. Como (int)(Math.log(x)/Math.log(2)+1e-10)nunca debería fallar. La elección de este "épsilon" no es una tarea trivial.

Más demostración, utilizando una tarea más general: tratar de implementar int log(int x, int base):

El código de prueba:

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);
        }
    }
}

Si usamos la implementación más directa de logaritmo,

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

esto imprime:

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
...

Para deshacerme completamente de los errores, tuve que agregar epsilon, que está entre 1e-11 y 1e-14. ¿Podría haber dicho esto antes de la prueba? Definitivamente no pude.


3
"no significa que funcionará de la misma manera en cualquier PC" - Lo haría si lo usaras strictfp, ¿no?
Ken

@Ken: Quizás ... Pero solo puede estar seguro después de enumerar exhaustivamente todos los valores de entrada posibles. (Tenemos suerte de que haya tan pocos de ellos aquí)
Rotsor

2
Técnicamente, sí, pero eso es cierto para cualquier función. En algún momento, debe confiar en que si utiliza la documentación disponible y prueba alguna fracción bien elegida pero muy pequeña de "todos los valores de entrada posibles", su programa funcionará lo suficientemente bien. strictfpParece que en realidad se ha vuelto un montón de basura por ser, de hecho, estricto. :-)
Ken

¿ return ((long)Math.log(x) / (long)Math.log(base));Qué tal si resolvemos todos los errores?
No es un error

92

Esta es la función que uso para este cálculo:

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 );
}

Es ligeramente más rápido que Integer.numberOfLeadingZeros () (20-30%) y casi 10 veces más rápido (jdk 1.6 x64) que una implementación basada en Math.log () como esta:

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 );
}

Ambas funciones devuelven los mismos resultados para todos los valores de entrada posibles.

Actualización: el JIT del servidor Java 1.7 puede reemplazar algunas funciones matemáticas estáticas con implementaciones alternativas basadas en intrínsecos de la CPU. Una de esas funciones es Integer.numberOfLeadingZeros (). Entonces, con una VM de servidor 1.7 o más reciente, una implementación como la de la pregunta es en realidad un poco más rápida que la binloganterior. Desafortunadamente, el cliente JIT no parece tener esta optimización.

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

Esta implementación también devuelve los mismos resultados para los 2 ^ 32 valores de entrada posibles que las otras dos implementaciones que publiqué anteriormente.

Estos son los tiempos de ejecución reales en mi PC (Sandy Bridge i7):

JDK 1.7 VM de cliente de 32 bits:

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

VM de servidor JDK 1.7 x64:

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

Este es el código de prueba:

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
La BSRinstrucción de x86 sí 32 - numberOfLeadingZeros, pero no está definida para 0, por lo que un compilador (JIT) tiene que verificar que no sea cero si no puede probar que no es necesario. Se introdujeron las extensiones del conjunto de instrucciones de BMI (Haswell y más recientes) LZCNT, que se implementa completamente numberOfLeadingZeros, en una sola instrucción. Ambos tienen latencia de 3 ciclos, 1 por rendimiento de ciclo. Así que recomiendo absolutamente usarlo numberOfLeadingZeros, porque eso hace que sea fácil para una buena JVM. (Lo único extraño lzcntes que tiene una dependencia falsa del valor anterior del registro que sobrescribe.)
Peter Cordes

Estoy más interesado en su comentario sobre los reemplazos intrínsecos de CPU JIT del servidor Java 1.7. ¿Tienes una URL de referencia? (El enlace del código fuente JIT también está bien.)
kevinarpe

37

Tratar Math.log(x) / Math.log(2)


8
Si bien matemáticamente esto es correcto, tenga en cuenta que existe un riesgo de error de cálculo debido a la aritmética de punto flotante imprecisa, como se explica en la respuesta de Rotsor.
leeyuiwah

28

puedes usar la identidad

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

entonces esto sería aplicable para log2.

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

simplemente conecte esto al método java Math log10 ...

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


3
Si bien matemáticamente esto es correcto, tenga en cuenta que existe un riesgo de error de cálculo debido a la aritmética de punto flotante imprecisa, como se explica en la respuesta de Rotsor.
leeyuiwah

18

Por qué no:

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

66
Si bien matemáticamente esto es correcto, tenga en cuenta que existe un riesgo de error de cálculo debido a la aritmética de punto flotante imprecisa, como se explica en la respuesta de Rotsor.
leeyuiwah

9

Existe la función en las bibliotecas de guayaba:

LongMath.log2()

Entonces sugiero usarlo.


¿Cómo puedo agregar este paquete a mi aplicación?
Elvin Mammadov

Descargue el jar desde aquí y agréguelo a la ruta de compilación de su proyecto.
Debosmit Ray

2
¿Debo agregar una biblioteca a mi aplicación solo para usar una función?
Tash Pemhiwa

77
¿Por qué exactamente sugerirías usarlo? Una lectura rápida de la fuente de Guava muestra que hace lo mismo que el método OP (unas pocas líneas de código muy claras), a costa de agregar una dependencia que de otro modo sería inútil. El hecho de que Google proporcione algo no lo hace mejor que comprender el problema y la solución usted mismo.
Dave

3

Para agregar a la respuesta x4u, que le da el piso del registro binario de un número, esta función devuelve el límite máximo del registro binario de un número:

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);
}

¿Dónde está la variable "número"?
barteks2x

3

Algunos casos simplemente funcionaron cuando usé Math.log10:

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

0

agreguemos:

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;
        }
    }
}

Fuente: https://github.com/pochuan/cs166/blob/master/ps1/rmq/SparseTableRMQ.java


Eso sería crear una tabla de búsqueda. El OP solicitó una forma más rápida de "calcular" un logaritmo.
Dave

-4

Para calcular la base de registro 2 de n, se puede usar la siguiente expresión:

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

2
Esta respuesta ya se ha publicado varias veces y ya se ha observado que es potencialmente imprecisa debido a un error de redondeo. Tenga en cuenta que el OP solicitó el valor integral; no está del todo claro qué precisión de redondeo debe usarse para llegar de aquí a un número entero.
AnotherParker
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.