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

Dispositivo nulo
fuente
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.

Rotsor
fuente
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 );
x4u
fuente
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)

Chris B.
fuente
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

hvgotcodes
fuente
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));
}
TofuBeer
fuente
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.

Demetr
fuente
¿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);
}
Ofek Ron
fuente
¿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));
}
Puerto pequeño
fuente
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

Guido Celada
fuente
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);
Akanksha
fuente
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