Come si calcola la base di registro 2 in Java per numeri interi?


138

Uso la seguente funzione per calcolare la base di registro 2 per gli interi:

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

Ha prestazioni ottimali?

Qualcuno conosce la funzione API J2SE pronta a tale scopo?

UPD1 Sorprendentemente per me, l'aritmetica in virgola mobile sembra essere più veloce dell'aritmetica intera.

UPD2 A causa di commenti, effettuerò indagini più dettagliate.

UPD3 La mia funzione aritmetica intera è 10 volte più veloce di Math.log (n) /Math.log (2).


1
Come hai testato le prestazioni di questo? Sul mio sistema (Core i7, jdk 1.6 x64) la versione intera è quasi 10 volte più veloce della versione a virgola mobile. Assicurati di fare effettivamente qualcosa con il risultato della funzione in modo che JIT non possa rimuovere del tutto il calcolo!
x4u,

Hai ragione. Non ho usato i risultati del calcolo e il compilatore ha ottimizzato qualcosa. Ora ho lo stesso risultato come te - la funzione intera è 10 volte più veloce (Core 2 Duo, jdk 1.6 c64)
Nulldevice

6
Questo ti dà effettivamente Math.floor(Math.log(n)/Math.log(2)), quindi non è davvero il calcolo della base di registro 2!
Dori,

Risposte:


74

Se stai pensando di utilizzare il virgola mobile per aiutare con l'aritmetica dei numeri interi, devi fare attenzione.

Di solito cerco di evitare i calcoli FP quando possibile.

Le operazioni in virgola mobile non sono esatte. Non puoi mai sapere con certezza cosa (int)(Math.log(65536)/Math.log(2))valuterà. Ad esempio, Math.ceil(Math.log(1<<29) / Math.log(2))è 30 sul mio PC dove matematicamente dovrebbe essere esattamente 29. Non ho trovato un valore per x dove (int)(Math.log(x)/Math.log(2))fallisce (solo perché ci sono solo 32 valori "pericolosi"), ma ciò non significa che funzionerà allo stesso modo su qualsiasi PC.

Il solito trucco qui è usare "epsilon" quando si arrotondano. Come (int)(Math.log(x)/Math.log(2)+1e-10)non dovrebbe mai fallire. La scelta di questo "epsilon" non è un compito banale.

Altre dimostrazioni, utilizzando un'attività più generale, cercando di implementare int log(int x, int base):

Il codice di test:

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

Se utilizziamo l'implementazione più diretta del logaritmo,

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

questo stampa:

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

Per eliminare completamente gli errori ho dovuto aggiungere epsilon tra 1e-11 e 1e-14. Avresti potuto dirlo prima del test? Non potrei assolutamente.


3
"non significa che funzionerà allo stesso modo su qualsiasi PC" - Se lo usassi strictfp, no?
Ken,

@Ken: Forse ... Ma puoi esserne sicuro solo dopo aver elencato esaurientemente tutti i possibili valori di input. (siamo fortunati che ce ne siano così pochi qui)
Rotsor,

2
Tecnicamente sì, ma è vero per qualsiasi funzione. Ad un certo punto devi fidarti del fatto che se usi la documentazione disponibile e collaudi una frazione ben scelta ma sparentemente piccola di "tutti i possibili valori di input", il tuo programma funzionerà abbastanza bene. strictfpsembra essersi davvero procurato un sacco di schifezze per essere, in effetti, severo. :-)
Ken

che ne dici return ((long)Math.log(x) / (long)Math.log(base));di risolvere tutti gli errori?
Non un bug,

92

Questa è la funzione che uso per questo calcolo:

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

È leggermente più veloce di Integer.numberOfLeadingZeros () (20-30%) e quasi 10 volte più veloce (jdk 1.6 x64) di un'implementazione basata su Math.log () come questa:

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

Entrambe le funzioni restituiscono gli stessi risultati per tutti i possibili valori di input.

Aggiornamento: JIT del server Java 1.7 è in grado di sostituire alcune funzioni matematiche statiche con implementazioni alternative basate su intrinseci della CPU. Una di queste funzioni è Integer.numberOfLeadingZeros (). Quindi, con una VM server 1.7 o più recente, un'implementazione come quella nella domanda è in realtà leggermente più veloce di quella binlogsopra. Sfortunatamente il client JIT non sembra avere questa ottimizzazione.

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

Questa implementazione restituisce anche gli stessi risultati per tutti i 2 ^ 32 possibili valori di input delle altre due implementazioni che ho pubblicato sopra.

Ecco i runtime effettivi sul mio PC (Sandy Bridge i7):

VM client JDK 1.7 a 32 bit:

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

VM del server JDK 1.7 x64:

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

Questo è il codice di prova:

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
L' BSRistruzione di x86 lo fa 32 - numberOfLeadingZeros, ma non è definito per 0, quindi un compilatore (JIT) deve controllare se è diverso da zero se non può provare che non è necessario. Sono state introdotte le estensioni del set di istruzioni BMI (Haswell e più recenti) LZCNT, che implementa numberOfLeadingZerosperfettamente in un'unica istruzione. Sono entrambi a 3 cicli di latenza, 1 per ciclo di produzione. Quindi consiglierei assolutamente di usarlo numberOfLeadingZeros, perché ciò semplifica una buona JVM. (L'unica cosa strana lzcntè che ha una falsa dipendenza dal vecchio valore del registro che sovrascrive.)
Peter Cordes,

Sono molto interessato al tuo commento sulle sostituzioni intrinseche della CPU JIT del server Java 1.7. Hai un URL di riferimento? (Anche il collegamento al codice sorgente JIT è OK.)
kevinarpe

37

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


8
Sebbene matematicamente ciò sia corretto, tieni presente che esiste un rischio di calcoli errati a causa dell'aritmetica in virgola mobile imprecisa, come spiegato nella risposta di Rotsor.
leeyuiwah,

28

puoi usare l'identità

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

quindi questo sarebbe applicabile per log2.

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

basta collegarlo al metodo java Math log10 ....

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


3
Sebbene matematicamente ciò sia corretto, tieni presente che esiste un rischio di calcoli errati a causa dell'aritmetica in virgola mobile imprecisa, come spiegato nella risposta di Rotsor.
leeyuiwah,

18

Perchè no:

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

6
Sebbene matematicamente ciò sia corretto, tieni presente che esiste un rischio di calcoli errati a causa dell'aritmetica in virgola mobile imprecisa, come spiegato nella risposta di Rotsor.
leeyuiwah,

9

C'è la funzione nelle librerie guava:

LongMath.log2()

Quindi suggerisco di usarlo.


Come posso aggiungere questo pacchetto alla mia applicazione?
Elvin Mammadov,

Scarica il vaso da qui e aggiungilo al percorso di costruzione del tuo progetto.
Debosmit Ray

2
Devo aggiungere una libreria nella mia applicazione solo per usare una funzione?
Tash Pemhiwa,

7
Perché consiglieresti di usarlo esattamente? Una rapida lettura della fonte di Guava mostra che fa la stessa cosa del metodo dell'OP (poche righe di codice molto chiaramente comprese), al costo di aggiungere una dipendenza altrimenti inutile. Solo perché Google fornisce qualcosa non lo rende migliore della comprensione del problema e della soluzione da soli.
Dave,

3

Per aggiungere alla risposta x4u, che fornisce il piano del registro binario di un numero, questa funzione restituisce il ceil del registro binario di un numero:

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

Dov'è la variabile "numero"?
barteks2x

3

Alcuni casi hanno funzionato solo quando ho usato Math.log10:

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

0

aggiungiamo:

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

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


Sarebbe creare una tabella di ricerca. L'OP ha chiesto un modo più veloce per "calcolare" un logaritmo.
Dave,

-4

Per calcolare la base di registro 2 di n, è possibile utilizzare la seguente espressione:

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

2
Questa risposta è già stata pubblicata più volte ed è già stata notata come potenzialmente inaccurata a causa di un errore di arrotondamento. Si noti che l'OP ha richiesto il valore integrale; non è affatto chiaro quale precisione di arrotondamento debba essere utilizzata per arrivare da qui a un numero intero.
AnotherParker,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.