Best practice per creare milioni di piccoli oggetti temporanei


109

Quali sono le "migliori pratiche" per creare (e rilasciare) milioni di piccoli oggetti?

Sto scrivendo un programma di scacchi in Java e l'algoritmo di ricerca genera un singolo oggetto "Move" per ogni possibile mossa, e una ricerca nominale può facilmente generare oltre un milione di oggetti in movimento al secondo. Il GC JVM è stato in grado di gestire il carico sul mio sistema di sviluppo, ma sono interessato a esplorare approcci alternativi che:

  1. Ridurre al minimo il sovraccarico della raccolta dei rifiuti e
  2. ridurre l'impronta di memoria massima per i sistemi di fascia bassa.

La stragrande maggioranza degli oggetti ha vita molto breve, ma circa l'1% delle mosse generate viene mantenuta e restituita come valore persistente, quindi qualsiasi tecnica di pooling o caching dovrebbe fornire la possibilità di escludere oggetti specifici dal riutilizzo .

Non mi aspetto un codice di esempio completo, ma apprezzerei suggerimenti per ulteriori letture / ricerche o esempi open source di natura simile.


11
Il modello Flyweight sarebbe appropriato per il tuo caso? en.wikipedia.org/wiki/Flyweight_pattern
Roger Rowland

4
Hai bisogno di incapsularlo in un oggetto?
nhahtdh

1
Il modello Flyweight non è appropriato, perché gli oggetti non condividono dati comuni significativi. Per quanto riguarda l'incapsulamento dei dati in un oggetto, è troppo grande per essere impacchettato in una primitiva, motivo per cui sto cercando alternative ai POJO.
Humble Programmer

2
Lettura altamente consigliata: cs.virginia.edu/kim/publicity/pldi09tutorials/…
rkj

Risposte:


47

Eseguire l'applicazione con Garbage Collection dettagliata:

java -verbose:gc

E ti dirà quando raccoglie. Ci sarebbero due tipi di sweep, uno veloce e uno completo.

[GC 325407K->83000K(776768K), 0.2300771 secs]
[GC 325816K->83372K(776768K), 0.2454258 secs]
[Full GC 267628K->83769K(776768K), 1.8479984 secs]

La freccia è prima e dopo la dimensione.

Finché si tratta solo di GC e non di un GC completo, sei al sicuro. Il normale GC è un collezionista di copie nella "giovane generazione", quindi gli oggetti a cui non si fa più riferimento vengono semplicemente dimenticati, che è esattamente quello che vorresti.

La lettura di Java SE 6 HotSpot Virtual Machine Garbage Collection Tuning è probabilmente utile.


Sperimenta con la dimensione dell'heap Java per cercare di trovare un punto in cui la raccolta completa dei rifiuti è rara. In Java 7 il nuovo GC G1 è più veloce in alcuni casi (e più lento in altri).
Michael Shops in

21

Dalla versione 6, la modalità server di JVM utilizza una tecnica di analisi dell'escape. Usandolo puoi evitare GC tutti insieme.


1
L'analisi della fuga spesso delude, vale la pena controllare se la JVM ha capito cosa stai facendo oppure no.
Nitsan Wakart

2
Se hai esperienza nell'utilizzo di queste opzioni: -XX: + PrintEscapeAnalysis e -XX: + PrintEliminateAllocations. Sarebbe fantastico condividere. Perché non lo dico onestamente.
Mikhail

vedi stackoverflow.com/questions/9032519/… dovrai ottenere una build di debug per JDK 7, ammetto di non averlo fatto ma con JDK 6 ha avuto successo.
Nitsan Wakart

19

Bene, ci sono diverse domande in una qui!

1 - Come vengono gestiti gli oggetti di breve durata?

Come affermato in precedenza, la JVM può gestire perfettamente una quantità enorme di oggetti di breve durata, poiché segue l' ipotesi generazionale debole .

Notare che stiamo parlando di oggetti che hanno raggiunto la memoria principale (heap). Non è sempre così. Molti oggetti che crei non lasciano nemmeno un registro della CPU. Ad esempio, considera questo ciclo for

for(int i=0, i<max, i++) {
  // stuff that implies i
}

Non pensiamo allo srotolamento del loop (un'ottimizzazione che la JVM esegue pesantemente sul tuo codice). Se maxè uguale a Integer.MAX_VALUE, l'esecuzione del ciclo potrebbe richiedere del tempo. comunque, ili variabile non sfuggirà mai al blocco del ciclo. Pertanto la JVM inserirà quella variabile in un registro della CPU, la incrementerà regolarmente ma non la rimanderà mai alla memoria principale.

Quindi, creare milioni di oggetti non è un grosso problema se vengono utilizzati solo localmente. Saranno morti prima di essere conservati in Eden, quindi il GC non li noterà nemmeno.

2 - È utile ridurre l'overhead del GC?

Come al solito, dipende.

Innanzitutto, è necessario abilitare la registrazione GC per avere una visione chiara di ciò che sta accadendo. Puoi abilitarlo con -Xloggc:gc.log -XX:+PrintGCDetails.

Se la tua applicazione trascorre molto tempo in un ciclo GC, allora, sì, ottimizza il GC, altrimenti potrebbe non valerne la pena.

Ad esempio, se hai un GC giovane ogni 100 ms che impiega 10 ms, trascorri il 10% del tuo tempo nel GC e hai 10 raccolte al secondo (che è enorme). In tal caso, non perderei tempo nella messa a punto del GC, poiché quei 10 GC / s sarebbero ancora lì.

3 - Qualche esperienza

Ho avuto un problema simile su un'applicazione che stava creando una quantità enorme di una determinata classe. Nei log GC, ho notato che la velocità di creazione dell'applicazione era di circa 3 GB / s, che è decisamente troppo (dai ... 3 gigabyte di dati al secondo?!).

Il problema: troppi GC frequenti causati dalla creazione di troppi oggetti.

Nel mio caso, ho collegato un profiler della memoria e ho notato che una classe rappresentava un'enorme percentuale di tutti i miei oggetti. Ho rintracciato le istanze per scoprire che questa classe era fondamentalmente una coppia di booleani avvolti in un oggetto. In quel caso, erano disponibili due soluzioni:

  • Rielabora l'algoritmo in modo da non restituire una coppia di booleani ma ho invece due metodi che restituiscono ogni booleano separatamente

  • Memorizza nella cache gli oggetti, sapendo che c'erano solo 4 istanze diverse

Ho scelto il secondo, in quanto ha avuto il minimo impatto sull'applicazione ed è stato facile da introdurre. Mi ci sono voluti pochi minuti per mettere una factory con una cache non thread-safe (non avevo bisogno di thread safety poiché alla fine avrei avuto solo 4 istanze diverse).

Il tasso di allocazione è sceso a 1 GB / s, così come la frequenza dei giovani GC (divisa per 3).

Spero che aiuti !


11

Se hai solo oggetti valore (cioè nessun riferimento ad altri oggetti) e davvero, ma intendo davvero tonnellate e tonnellate, puoi usarli direttamente ByteBufferscon l'ordinamento nativo dei byte [quest'ultimo è importante] e hai bisogno di alcune centinaia di righe di codice per allocare / riutilizzare + getter / setters. I Getters sono simili along getQuantity(int tupleIndex){return buffer.getLong(tupleInex+QUANTITY_OFFSSET);}

Ciò risolverebbe il problema del GC quasi completamente a patto di allocare una sola volta, ovvero una parte enorme e quindi gestire gli oggetti da soli. Invece di riferimenti avresti solo index (cioè int) nel fileByteBuffer che deve essere passato. Potrebbe essere necessario allineare la memoria anche da soli.

La tecnica sarebbe come usare C and void*, ma con un po 'di wrapping è sopportabile. Uno svantaggio delle prestazioni potrebbe essere il controllo dei limiti se il compilatore non riesce a eliminarlo. Un vantaggio importante è la località se si elaborano le tuple come vettori, la mancanza dell'intestazione dell'oggetto riduce anche l'impronta di memoria.

Oltre a questo, è probabile che non avresti bisogno di un tale approccio poiché la giovane generazione di quasi tutte le JVM muore banalmente e il costo di allocazione è solo un aumento del puntatore. Il costo di allocazione può essere un po 'più alto se si utilizzano i finalcampi in quanto richiedono un limite di memoria su alcune piattaforme (ovvero ARM / Power), su x86 è gratuito, però.


8

Supponendo che trovi GC è un problema (come altri sottolineano che potrebbe non essere) implementerai la tua gestione della memoria per te caso speciale, ad esempio una classe che soffre di un enorme abbandono. Prova il pool di oggetti, ho visto casi in cui funziona abbastanza bene. L'implementazione di pool di oggetti è un percorso ben battuto, quindi non è necessario visitare nuovamente qui, fare attenzione a:

  • multi-threading: l'utilizzo di pool locali di thread potrebbe funzionare per il tuo caso
  • struttura dei dati di supporto: prendere in considerazione l'utilizzo di ArrayDeque poiché funziona bene durante la rimozione e non ha sovraccarico di allocazione
  • limita le dimensioni della tua piscina :)

Misura prima / dopo ecc. Ecc


6

Ho incontrato un problema simile. Prima di tutto, prova a ridurre le dimensioni dei piccoli oggetti. Abbiamo introdotto alcuni valori di campo predefiniti che fanno riferimento ad essi in ogni istanza di oggetto.

Ad esempio, MouseEvent ha un riferimento alla classe Point. Abbiamo memorizzato nella cache i punti e li abbiamo referenziati invece di creare nuove istanze. Lo stesso per, ad esempio, le stringhe vuote.

Un'altra fonte era più booleani che sono stati sostituiti con un int e per ogni booleano usiamo solo un byte dell'int.


Solo per interesse: cosa ti ha acquistato in termini di prestazioni? Hai profilato la tua domanda prima e dopo la modifica e, in caso affermativo, quali sono stati i risultati?
Axel

@ Axel gli oggetti utilizzano molta meno memoria, quindi GC non viene chiamato così spesso. Sicuramente abbiamo profilato la nostra app ma c'è stato anche un effetto visivo della maggiore velocità.
StanislavL

6

Ho affrontato questo scenario con del codice di elaborazione XML qualche tempo fa. Mi sono ritrovato a creare milioni di oggetti tag XML che erano molto piccoli (di solito solo una stringa) ed estremamente di breve durata (il fallimento di un controllo XPath significava nessuna corrispondenza, quindi scartalo).

Ho fatto alcuni test seri e sono giunto alla conclusione che potevo ottenere solo un miglioramento di circa il 7% sulla velocità utilizzando un elenco di tag scartati invece di crearne di nuovi. Tuttavia, una volta implementata, ho scoperto che la coda libera necessitava di un meccanismo aggiunto per eliminarla se diventava troppo grande - questo ha completamente annullato la mia ottimizzazione, quindi l'ho cambiata in un'opzione.

In sintesi, probabilmente non ne vale la pena, ma sono felice di vedere che ci stai pensando, dimostra che ci tieni.


2

Dato che stai scrivendo un programma di scacchi, ci sono alcune tecniche speciali che puoi usare per prestazioni decenti. Un approccio semplice consiste nel creare una vasta gamma di long (o byte) e trattarlo come uno stack. Ogni volta che il tuo generatore di mosse crea mosse, spinge un paio di numeri sulla pila, ad esempio sposta da una casella e sposta in una casella. Mentre valuti l'albero di ricerca, farai scoppiare le mosse e aggiornerai una rappresentazione della scacchiera.

Se vuoi un potere espressivo usa gli oggetti. Se vuoi la velocità (in questo caso) diventa nativo.


1

Una soluzione che ho usato per tali algoritmi di ricerca è creare un solo oggetto Move, modificarlo con una nuova mossa e quindi annullare la mossa prima di lasciare l'ambito. Probabilmente stai analizzando solo una mossa alla volta e poi stai semplicemente memorizzando la mossa migliore da qualche parte.

Se questo non è fattibile per qualche motivo e vuoi ridurre il picco di utilizzo della memoria, un buon articolo sull'efficienza della memoria è qui: http://www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-efficient-java- Tutorial.Pdf


Collegamento morto. C'è un'altra fonte per quell'articolo?
dal

0

Crea i tuoi milioni di oggetti e scrivi il codice nel modo corretto: non conservare riferimenti inutili a questi oggetti. GC farà il lavoro sporco per te. Puoi giocare con GC prolisso come menzionato per vedere se sono davvero GC. Java riguarda la creazione e il rilascio di oggetti. :)


1
Scusa amico, non sono d'accordo con il tuo approccio ... Java, come qualsiasi linguaggio di programmazione, riguarda la risoluzione di un problema entro i suoi vincoli, se l'OP è vincolato da GC come lo aiuti?
Nitsan Wakart

1
Gli sto dicendo come funziona effettivamente Java. Se non è in grado di schivare la situazione di avere milioni di oggetti temporanei, il miglior consiglio potrebbe essere, la classe temporanea dovrebbe essere leggera e deve assicurarsi di rilasciare i riferimenti il ​​prima possibile, non più un singolo passaggio. Mi sto perdendo qualcosa?
gyorgyabraham

Java supporta la creazione di spazzatura e la pulirà per te, questo è vero. Se l'OP non può schivare la creazione di oggetti ed è scontento del tempo trascorso in GC, è un finale triste. La mia obiezione è alla raccomandazione che fai di lavorare di più per GC perché in qualche modo è Java appropriato.
Nitsan Wakart

0

Penso che dovresti leggere sull'allocazione dello stack in Java e sull'analisi degli escape.

Perché se approfondisci questo argomento potresti scoprire che i tuoi oggetti non sono nemmeno allocati sull'heap e non vengono raccolti da GC come lo sono gli oggetti sull'heap.

C'è una spiegazione su wikipedia dell'analisi di fuga, con un esempio di come funziona in Java:

http://en.wikipedia.org/wiki/Escape_analysis


0

Non sono un grande fan di GC, quindi cerco sempre di trovare modi per aggirarlo. In questo caso suggerirei di utilizzare il pattern Object Pool :

L'idea è di evitare di creare nuovi oggetti conservandoli in una pila in modo da poterli riutilizzare in seguito.

Class MyPool
{
   LinkedList<Objects> stack;

   Object getObject(); // takes from stack, if it's empty creates new one
   Object returnObject(); // adds to stack
}

3
Usare il pool per piccoli oggetti è una pessima idea, è necessario un pool per thread per l'avvio (o l'accesso condiviso uccide qualsiasi prestazione). Tali pool hanno anche prestazioni peggiori di un buon netturbino. Ultimo: il GC è una manna dal cielo quando si ha a che fare con codice / strutture concorrenti: molti algoritmi sono significativamente più facili da implementare poiché naturalmente non ci sono problemi ABA. Ref. il conteggio in ambiente simultaneo richiede almeno un'operazione atomica + barriera di memoria (LOCK ADD o CAS su x86)
bestsss

1
La gestione degli oggetti nel pool può essere più costosa rispetto all'esecuzione del Garbage Collector.
Thorbjørn Ravn Andersen

@ ThorbjørnRavnAndersen In generale sono d'accordo con te, ma nota che rilevare tale differenza è una vera sfida e quando arrivi alla conclusione che GC funziona meglio nel tuo caso, deve essere un caso unico se tale differenza è importante. Tuttavia, al contrario, potrebbe essere che il pool di oggetti salverà la tua app.
Ilya Gazman

1
Semplicemente non capisco il tuo argomento? È molto difficile rilevare se GC è più veloce del pool di oggetti? E quindi dovresti usare il pool di oggetti? La JVM è ottimizzata per una codifica pulita e oggetti di breve durata. Se questo è ciò di cui tratta questa domanda (che spero se OP ne genera un milione al secondo), allora dovrebbe essere solo se c'è un vantaggio dimostrabile per passare a uno schema più complesso e soggetto a errori come quello che suggerisci. Se questo è troppo difficile da dimostrare, perché preoccuparsi.
Thorbjørn Ravn Andersen

0

I pool di oggetti forniscono miglioramenti enormi (a volte 10 volte) rispetto all'allocazione di oggetti nell'heap. Ma l'implementazione di cui sopra utilizzando un elenco collegato è sia ingenua che sbagliata! La lista collegata crea oggetti per gestire la sua struttura interna annullando lo sforzo. Un Ringbuffer che utilizza una serie di oggetti funziona bene. Nell'esempio give (un programma di scacchi che gestisce le mosse) il Ringbuffer dovrebbe essere racchiuso in un oggetto contenitore per l'elenco di tutte le mosse calcolate. Verrebbero quindi passati solo i riferimenti all'oggetto portatore di mosse.

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.