Dichiarazione di più array con 64 elementi 1000 volte più veloce rispetto alla dichiarazione di array di 65 elementi


91

Recentemente ho notato che dichiarare un array contenente 64 elementi è molto più veloce (> 1000 volte) rispetto a dichiarare lo stesso tipo di array con 65 elementi.

Ecco il codice che ho usato per testarlo:

public class Tests{
    public static void main(String args[]){
        double start = System.nanoTime();
        int job = 100000000;//100 million
        for(int i = 0; i < job; i++){
            double[] test = new double[64];
        }
        double end = System.nanoTime();
        System.out.println("Total runtime = " + (end-start)/1000000 + " ms");
    }
}

Questo viene eseguito in circa 6 ms, se sostituisco new double[64]con new double[65]ci vogliono circa 7 secondi. Questo problema diventa esponenzialmente più grave se il lavoro viene distribuito su un numero sempre maggiore di thread, da cui ha origine il mio problema.

Questo problema si verifica anche con diversi tipi di matrici come int[65]o String[65]. Questo problema non si verifica con stringhe di grandi dimensioni:, String test = "many characters";ma inizia a verificarsi quando viene modificato inString test = i + "";

Mi chiedevo perché è così e se è possibile aggirare questo problema.


3
Fuori nota: System.nanoTime()dovrebbe essere preferito System.currentTimeMillis()per il benchmarking.
rocketboy

4
Sono solo curioso ? Sei sotto Linux? Il comportamento cambia con il sistema operativo?
bsd

9
Come diavolo ha fatto questa domanda a ottenere un voto negativo?
Rohit Jain

2
FWIW, vedo discrepanze simili nelle prestazioni se eseguo questo codice con byteinvece di double.
Oliver Charlesworth

3
@ThomasJungblut: Allora cosa spiega la discrepanza nell'esperimento dell'OP?
Oliver Charlesworth

Risposte:


88

Stai osservando un comportamento causato dalle ottimizzazioni eseguite dal compilatore JIT della tua Java VM. Questo comportamento è riproducibile attivato con matrici scalari fino a 64 elementi e non viene attivato con matrici più grandi di 64.

Prima di entrare nei dettagli, diamo un'occhiata più da vicino al corpo del loop:

double[] test = new double[64];

Il corpo non ha alcun effetto (comportamento osservabile) . Ciò significa che non fa differenza al di fuori dell'esecuzione del programma se questa istruzione viene eseguita o meno. Lo stesso vale per l'intero ciclo. Quindi potrebbe accadere che l'ottimizzatore del codice traduca il ciclo in qualcosa (o niente) con lo stesso comportamento di temporizzazione funzionale e diverso.

Per i benchmark dovresti almeno aderire alle seguenti due linee guida. Se lo avessi fatto, la differenza sarebbe stata notevolmente inferiore.

  • Riscaldare il compilatore (e l'ottimizzatore) JIT eseguendo più volte il benchmark.
  • Usa il risultato di ogni espressione e stampalo alla fine del benchmark.

Adesso entriamo nei dettagli. Non sorprende che ci sia un'ottimizzazione che viene attivata per array scalari non più grandi di 64 elementi. L'ottimizzazione fa parte dell'analisi Escape . Mette piccoli oggetti e piccoli array nello stack invece di allocarli nell'heap, o meglio ancora ottimizzarli del tutto. Puoi trovare alcune informazioni a riguardo nel seguente articolo di Brian Goetz scritto nel 2005:

L'ottimizzazione può essere disabilitata con l'opzione della riga di comando -XX:-DoEscapeAnalysis. Il valore magico 64 per gli array scalari può anche essere modificato dalla riga di comando. Se esegui il programma come segue, non ci saranno differenze tra gli array con 64 e 65 elementi:

java -XX:EliminateAllocationArraySizeLimit=65 Tests

Detto questo, sconsiglio vivamente di utilizzare tali opzioni della riga di comando. Dubito che faccia un'enorme differenza in un'applicazione realistica. Lo userei solo se fossi assolutamente convinto della necessità - e non basandomi sui risultati di alcuni pseudo benchmark.


9
Ma perché l'ottimizzatore rileva che l'array di dimensione 64 è rimovibile ma non 65
ug_

10
@nosid: sebbene il codice dell'OP potrebbe non essere realistico, sta chiaramente innescando un comportamento interessante / inaspettato nella JVM, che potrebbe avere implicazioni in altre situazioni. Penso che sia valido chiedersi perché sta accadendo.
Oliver Charlesworth

1
@ThomasJungblut Non credo che il ciclo venga rimosso. Puoi aggiungere "int total" fuori dal ciclo e aggiungere "total + = test [0];" all'esempio sopra. Quindi stampando il risultato vedrai che il totale = 100 milioni e verrà eseguito in meno di un secondo.
Sipko

1
La sostituzione sullo stack riguarda la sostituzione del codice interpretato con quello compilato al volo, invece di sostituire l'allocazione dell'heap con l'allocazione dello stack. EliminateAllocationArraySizeLimit è la dimensione limite degli array considerati scalari sostituibili nell'analisi di escape. Quindi il punto principale che l'effetto è dovuto all'ottimizzazione del compilatore è corretto, ma non è dovuto all'allocazione dello stack, ma a causa della fase di analisi di fuga che non riesce a notare che l'allocazione non è necessaria.
kiheru

2
@ Sipko: Stai scrivendo che l'applicazione non viene ridimensionata con il numero di thread. Questa è un'indicazione che il problema non è correlato alle micro ottimizzazioni di cui stai chiedendo. Consiglio di guardare il quadro generale anziché le parti piccole.
nosid

2

Ci sono molti modi in cui ci può essere una differenza, in base alle dimensioni di un oggetto.

Come affermato da nosid, il JITC potrebbe (molto probabilmente) allocare piccoli oggetti "locali" sullo stack, e il limite di dimensione per array "piccoli" può essere a 64 elementi.

L'allocazione sullo stack è notevolmente più veloce dell'allocazione nell'heap e, soprattutto, lo stack non deve essere sottoposto a garbage collection, quindi l'overhead GC viene notevolmente ridotto. (E per questo caso di test il sovraccarico GC è probabilmente l'80-90% del tempo di esecuzione totale.)

Inoltre, una volta che il valore è stato allocato nello stack, JITC può eseguire l '"eliminazione del codice inattivo", determinare che il risultato di newnon viene mai utilizzato da nessuna parte e, dopo essersi assicurati che non ci siano effetti collaterali che andrebbero persi, eliminare l'intera newoperazione, e poi il ciclo (ora vuoto) stesso.

Anche se JITC non esegue l'allocazione dello stack, è del tutto possibile che oggetti più piccoli di una certa dimensione vengano allocati in un heap in modo diverso (ad esempio, da uno "spazio" diverso) rispetto a oggetti più grandi. (Normalmente questo non produrrebbe differenze temporali così drammatiche, però.)


In ritardo a questo thread. Perché l'allocazione nello stack è più veloce dell'allocazione nell'heap? Secondo alcuni articoli, l'allocazione sull'heap richiede ~ 12 istruzioni. Non c'è molto spazio per migliorare.
Vortex

@Vortex - L'allocazione allo stack richiede 1-2 istruzioni. Ma questo è per allocare un intero stack frame. Lo stack frame deve essere comunque allocato per avere un'area di salvataggio dei registri per la routine, quindi tutte le altre variabili allocate contemporaneamente sono "libere". E come ho detto, lo stack non richiede GC. L'overhead GC per un elemento di heap è molto maggiore del costo dell'operazione di allocazione di heap.
Hot Licks
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.