Allocazione di heap Java più veloce di C ++


13

Ho già pubblicato questa domanda su SO e ha funzionato bene. Purtroppo è stato chiuso (per riaprire è necessario solo un voto), ma qualcuno mi ha suggerito di pubblicarlo qui perché si adatta meglio, quindi ciò che segue è letteralmente una copia della domanda


Stavo leggendo i commenti su questa risposta e ho visto questa citazione.

L'istanza e le funzionalità orientate agli oggetti sono incredibilmente veloci da usare (più veloce di C ++ in molti casi) perché sono progettate dall'inizio. e le raccolte sono veloci. Java standard batte C / C ++ standard in quest'area, anche per la maggior parte del codice C ottimizzato.

Un utente (con un alto livello di reputazione potrei aggiungere) ha coraggiosamente difeso questa affermazione, affermando che

  1. l'allocazione di heap in java è migliore di quella di C ++

  2. e ha aggiunto questa affermazione difendendo le raccolte in Java

    E le raccolte Java sono veloci rispetto alle raccolte C ++ grazie in gran parte al diverso sottosistema di memoria.

Quindi la mia domanda è: tutto ciò può essere vero, e se sì, perché l'allocazione dell'heap di java è molto più veloce.


Puoi trovare la mia risposta a una domanda simile su SO utile / pertinente.
Daniel Pryden,

1
È banale: con Java (o qualsiasi altro ambiente gestito e limitato) è possibile spostare oggetti e aggiornare i puntatori su di essi, ovvero ottimizzare in modo dinamico per una migliore localizzazione della cache. Con C ++ e il suo puntatore aritmetica con bitcast incontrollati tutti gli oggetti sono bloccati nella loro posizione per sempre.
Logica SK

3
Non avrei mai pensato di sentire qualcuno dire che la gestione della memoria Java è più veloce perché copia la memoria tutto il tempo. sospiro.
gbjbaanb,

1
@gbjbaanb, hai mai sentito parlare della gerarchia della memoria? Pena mancata penalità? Ti rendi conto che un allocatore per scopi generici è costoso, mentre un allocazione di prima generazione è solo una singola operazione di addizione?
Logica SK

1
Anche se questo può essere un po 'vero in alcuni casi, manca il punto che in java si alloca tutto sull'heap e in c ++ si alloca una grande quantità di oggetti nello stack che può essere ancora molto più veloce.
Giovanni B

Risposte:


23

Questa è una domanda interessante e la risposta è complessa.

Nel complesso, penso che sia giusto dire che il Garbage Collector di JVM è progettato molto bene ed estremamente efficiente. È probabilmente il miglior sistema di gestione della memoria per scopi generici .

Il C ++ può battere il GC JVM con allocatori di memoria specializzati progettati per scopi specifici. Esempi potrebbero essere:

  • Allocatori di memoria per fotogramma, che cancellano l'intera area di memoria a intervalli periodici. Questi sono frequentemente utilizzati nei giochi C ++, ad esempio, in cui un'area di memoria temporanea viene utilizzata una volta per frame e immediatamente scartata.
  • Allocatori personalizzati che gestiscono un pool di oggetti di dimensioni fisse
  • Allocazione basata sullo stack (sebbene si noti che la JVM lo fa anche in varie circostanze, ad esempio tramite analisi di escape )

Gli allocatori di memoria specializzati sono ovviamente limitati per definizione. Di solito hanno restrizioni sul ciclo di vita degli oggetti e / o restrizioni sul tipo di oggetto che può essere gestito. La raccolta dei rifiuti è molto più flessibile.

La garbage collection offre anche alcuni vantaggi significativi dal punto di vista delle prestazioni:

  • L' istanza dell'oggetto è davvero estremamente veloce. A causa del modo in cui i nuovi oggetti sono allocati in sequenza in memoria, spesso richiede poco più di un'aggiunta di puntatore, che è certamente più veloce dei tipici algoritmi di allocazione dell'heap C ++.
  • Si evita la necessità di costi di gestione del ciclo di vita - ad es. Il conteggio dei riferimenti (talvolta usato come alternativa a GC) è estremamente scarso dal punto di vista delle prestazioni poiché il frequente aumento e decremento dei conteggi di riferimento aggiunge un sacco di costi generali delle prestazioni (in genere molto più di GC) .
  • Se si utilizzano oggetti immutabili, è possibile sfruttare la condivisione strutturale per risparmiare memoria e migliorare l'efficienza della cache. Questo è ampiamente utilizzato da linguaggi funzionali sulla JVM come Scala e Clojure. È molto difficile farlo senza GC, perché è estremamente difficile gestire le vite degli oggetti condivisi. Se ritieni (come faccio io) che l'immutabilità e la condivisione strutturale siano fondamentali per la creazione di applicazioni simultanee di grandi dimensioni, questo è probabilmente il più grande vantaggio in termini di prestazioni di GC.
  • È possibile evitare la copia se tutti i tipi di oggetto e i rispettivi cicli di vita sono gestiti dallo stesso sistema di garbage collection. Contrasto con C ++, dove spesso è necessario eseguire copie complete dei dati perché la destinazione richiede un approccio di gestione della memoria diverso o ha un ciclo di vita degli oggetti diverso.

Java GC ha un grande svantaggio: poiché il lavoro di raccolta dei rifiuti è rinviato e fatto in blocchi di lavoro a intervalli periodici, provoca pause GC occasionali per raccogliere i rifiuti, il che può influire sulla latenza. Questo di solito non è un problema per le applicazioni tipiche, ma può escludere Java in situazioni in cui è richiesto un hard realtime (ad es. Controllo robotico). Il soft realtime (ad es. Giochi, multimedia) è in genere OK.


ci sono librerie specializzate nell'area c ++ che risolvono questo problema. L'esempio probabilmente più famoso per questo è SmartHeap.
Tobias Langner,

5
Il soft-realtime non significa che di solito ti fermi . Significa solo che puoi mettere in pausa / riprovare in una brutta situazione , di solito inaspettata, invece di arrestare / arrestare / fallire. Nessuno vorrebbe usare di solito il lettore musicale in pausa. Il problema della pausa GC è che accade di solito e imprevedibilmente . In tal modo, la pausa GC non è accettabile nemmeno per l'applicazione soft-realtime. La pausa GC è accettabile solo quando agli utenti non interessa la qualità dell'applicazione. E al giorno d'oggi, le persone non sono più così ingenue.
Eonil,

1
Si prega di pubblicare alcune misurazioni delle prestazioni per supportare le vostre affermazioni, altrimenti stiamo confrontando mele e arance.
JBR Wilkinson,

1
@Demetri Ma in realtà, ciò accade solo se il caso accade troppo (e ancora, anche imprevedibilmente!) A meno che tu non possa soddisfare alcuni vincoli poco pratici. In altre parole, C ++ è molto più semplice per qualsiasi situazione in tempo reale.
Eonil,

1
Per completezza: c'è un altro aspetto negativo del GC dal punto di vista delle prestazioni: come nella maggior parte dei GC esistenti la liberazione della memoria si verifica in un altro thread che probabilmente verrà eseguito su un core diverso, significa che i GC stanno sostenendo gravi costi di invalidazione della cache per la sincronizzazione Cache L1 / L2 tra core diversi; inoltre, su server prevalentemente NUMA, anche le cache L3 devono essere sincronizzate (e su Hypertransport / QPI, ouch (!)).
No-Bugs Hare,

3

Questa non è un'affermazione scientifica. Sto semplicemente dando un po 'di spunti di riflessione su questo problema.

Un'analogia visiva è questa: ti viene dato un appartamento (un'unità residenziale) con moquette. Il tappeto è sporco. Qual è il modo più veloce (in termini di ore) per rendere il pavimento dell'appartamento perfettamente pulito?

Risposta: arrotoli semplicemente il vecchio tappeto; Buttar via; e stendere un nuovo tappeto.

Cosa stiamo trascurando qui?

  • Il costo del trasferimento di oggetti personali esistenti e il successivo trasferimento.
    • Questo è noto come il costo "stop-the-world" della raccolta dei rifiuti.
  • Il costo del nuovo tappeto.
    • Che, per coincidenza per la RAM, è gratuito.

La garbage collection è un argomento enorme e ci sono molte domande sia in Programmers.SE che StackOverflow.

Da un lato, un gestore di allocazione C / C ++ noto come TCMalloc insieme al conteggio dei riferimenti agli oggetti è teoricamente in grado di soddisfare le migliori richieste di prestazioni di qualsiasi sistema GC.


in realtà c ++ 11 ha anche una raccolta dati inutili ABI , questo è abbastanza simile ad alcune delle risposte che ho avuto su SO
aaronman,

È il timore di violare i programmi C / C ++ esistenti (basi di codice, come kernel Linux e librerie archaic_but_still_economically_important come libtiff) che ha ostacolato il progresso dell'innovazione del linguaggio in C ++.
rwong,

Ha senso, immagino che in c ++ 17 sarà più completo, ma la verità è che una volta che impari davvero a programmare in c ++ non lo vuoi nemmeno più, forse possono trovare un modo per combinare i due modi di dire bene
aaronman il

Ti rendi conto che ci sono spazzatura che non fermano il mondo? Hai preso in considerazione le implicazioni in termini di prestazioni della compactificazione (lato GC) e della frammentazione dell'heap (per allocatori C ++ generici)?
Logica SK

2
Penso che il principale difetto di questa analogia sia che quello che GC fa effettivamente è trovare i pezzi sporchi, ritagliarli e quindi vedere i pezzi rimanenti insieme per creare un nuovo tappeto.
svick

3

Il motivo principale è che, quando chiedi a Java un nuovo grumo di memoria, va dritto alla fine dell'heap e ti dà un blocco. In questo modo, l'allocazione della memoria è veloce come l'allocazione nello stack (che è il modo in cui lo si fa la maggior parte delle volte in C / C ++, ma a parte questo ..)

Quindi le allocazioni sono veloci come qualsiasi cosa ma ... ciò non conta il costo di liberare la memoria. Solo perché non libererai nulla fino a molto tempo dopo non significa che non costa molto, e nel caso del sistema GC, il costo è molto più delle allocazioni di heap "normali" - non solo il GC deve scorrere tutti gli oggetti per vedere se sono vivi o meno, quindi deve anche liberarli e (il grande costo) copiare la memoria per compattare l'heap - in modo da poter avere l'allocazione rapida alla fine meccanismo (o si esaurirebbe la memoria, ad esempio C / C ++ eseguirà l'heap su ogni allocazione alla ricerca del blocco successivo di spazio libero che possa adattarsi all'oggetto).

Questo è uno dei motivi per cui i benchmark Java / .NET mostrano prestazioni così buone, ma le applicazioni del mondo reale mostrano prestazioni così scarse. Devo solo guardare le app sul mio telefono - quelle molto veloci e reattive sono tutte scritte usando l'NDK, tanto che anche io sono rimasto sorpreso.

Le raccolte al giorno d'oggi possono essere veloci se tutti gli oggetti sono allocati localmente, ad esempio in un singolo blocco contiguo. Ora, in Java, semplicemente non si ottengono blocchi contigui poiché gli oggetti vengono allocati uno alla volta dall'estremità libera dell'heap. Puoi finire con loro felicemente contigui, ma solo per fortuna (cioè per il capriccio delle routine di compattazione GC e come copia gli oggetti). C / C ++ invece supporta esplicitamente allocazioni contigue (tramite lo stack, ovviamente). Generalmente gli oggetti heap in C / C ++ non sono diversi dal BTW di Java.

Ora con C / C ++ puoi migliorare rispetto agli allocatori predefiniti progettati per risparmiare memoria e utilizzarla in modo efficiente. È possibile sostituire l'allocatore con un set di pool a blocchi fissi, in modo da trovare sempre un blocco della dimensione esatta per l'oggetto che si sta allocando. Camminare per l'heap diventa solo una questione di ricerca bitmap per vedere dove si trova un blocco libero e la de-allocazione è semplicemente reimpostare un po 'in quella bitmap. Il costo è che si utilizza più memoria durante l'allocazione in blocchi di dimensioni fisse, quindi si ha un mucchio di blocchi da 4 byte, un altro per blocchi da 16 byte, ecc.


2
Sembra che tu non capisca affatto i GC. Considera lo scenario più tipico: centinaia di piccoli oggetti vengono costantemente allocati, ma solo una dozzina di essi sopravviverà per più di un secondo. In questo modo, non vi è assolutamente alcun costo nel liberare la memoria: questa dozzina viene copiata dalla giovane generazione (e compattata, come ulteriore vantaggio), e il resto viene scartato senza alcun costo. E, a proposito, il patetico GC Dalvik non ha nulla a che fare con i moderni GC all'avanguardia che troverai nelle giuste implementazioni JVM.
SK-logic,

1
Se uno di quegli oggetti liberati si trova nel mezzo dell'heap, il resto dell'heap verrà compattato per recuperare lo spazio. O stai dicendo che la compattazione GC non avviene a meno che non sia il caso migliore che descrivi? So che i GC generazionali fanno molto meglio qui, a meno che non rilasci un oggetto nel mezzo delle generazioni successive, nel qual caso l'impatto può essere relativamente grande. C'era qualcosa scritto da un Microsoftie che lavorava sul proprio GC che ho letto che descriveva i compromessi del GC quando si creava un GC generazionale. Vedrò se riesco a trovarlo di nuovo.
gbjbaanb,

1
Di quale "mucchio" stai parlando? La maggior parte della spazzatura viene recuperata nella fase delle giovani generazioni e la maggior parte dei benefici in termini di prestazioni proviene esattamente da quella compattazione. Naturalmente, è principalmente visibile su un profilo di allocazione della memoria tipico per la programmazione funzionale (molti piccoli oggetti di breve durata). E, naturalmente, ci sono numerose opportunità di ottimizzazione non ancora del tutto esplorate, ad esempio un'analisi dinamica della regione che può trasformare automaticamente le allocazioni di heap in un determinato percorso in allocazioni di stack o pool.
SK-logic,

3
Non sono d'accordo con la tua affermazione che l'allocazione dell'heap è "veloce come lo stack" - l'allocazione dell'heap richiede la sincronizzazione dei thread e lo stack no (per definizione)
JBRWilkinson

1
Immagino di sì, ma con Java e .net vedi il mio punto - non devi camminare nell'heap per trovare il prossimo blocco gratuito, quindi è significativamente più veloce in questo senso, ma sì - hai ragione, deve essere bloccato che danneggerà le app thread.
gbjbaanb,

2

Eden Space

Quindi la mia domanda è: tutto ciò può essere vero, e se sì, perché l'allocazione dell'heap di java è molto più veloce.

Ho studiato un po 'su come funziona Java GC poiché è molto interessante per me. Cerco sempre di espandere la mia raccolta di strategie di allocazione della memoria in C e C ++ (interessato a provare a implementare qualcosa di simile in C), ed è un modo molto, molto veloce per allocare molti oggetti in modo rapido da un prospettiva pratica ma principalmente dovuta al multithreading.

Il modo in cui funziona l'allocazione del GC Java consiste nell'utilizzare una strategia di allocazione estremamente economica per allocare inizialmente gli oggetti nello spazio "Eden". Da quello che posso dire, sta usando un allocatore di pool sequenziale.

È molto più veloce solo in termini di algoritmo e di riduzione degli errori di pagina obbligatori rispetto a quelli generici mallocin C o predefiniti, operator newin C ++.

Ma gli allocatori sequenziali hanno un evidente punto debole: possono allocare blocchi di dimensioni variabili, ma non possono liberare alcun singolo blocco. Si allocano semplicemente in modo sequenziale con imbottitura per l'allineamento e possono solo eliminare tutta la memoria allocata in una sola volta. Sono utili in genere in C e C ++ per la costruzione di strutture di dati che richiedono solo inserimenti e nessuna rimozione di elementi, come un albero di ricerca che deve essere creato solo una volta all'avvio di un programma e quindi viene ripetutamente cercato o sono state aggiunte solo nuove chiavi ( nessuna chiave rimossa).

Possono anche essere utilizzati anche per le strutture di dati che consentono la rimozione di elementi, ma tali elementi non verranno effettivamente liberati dalla memoria poiché non è possibile allocarli singolarmente. Una struttura del genere che utilizza un allocatore sequenziale consumerebbe solo sempre più memoria, a meno che non avesse un passaggio differito in cui i dati venivano copiati in una nuova copia compatta usando un allocatore sequenziale separato (e questa è talvolta una tecnica molto efficace se un allocatore fisso ha vinto per qualche motivo, basta semplicemente allocare in sequenza una nuova copia della struttura dei dati e scaricare tutta la memoria di quella vecchia).

Collezione

Come nell'esempio di struttura di dati / pool sequenziale sopra, sarebbe un grosso problema se Java GC fosse allocato solo in questo modo, anche se è super veloce per un'allocazione a raffica di molti singoli blocchi. Non sarebbe in grado di liberare nulla fino allo spegnimento del software, a quel punto potrebbe liberare (eliminare) tutti i pool di memoria contemporaneamente.

Quindi, invece, dopo un singolo ciclo GC, viene effettuato un passaggio attraverso oggetti esistenti nello spazio "Eden" (allocati in sequenza), e quelli a cui si fa ancora riferimento vengono allocati utilizzando un allocatore più generico in grado di liberare singoli blocchi. Quelli a cui non si fa più riferimento verranno semplicemente dislocati nel processo di spurgo. Quindi in pratica si tratta di "copiare oggetti dallo spazio Eden se sono ancora citati e quindi eliminare".

Questo sarebbe normalmente piuttosto costoso, quindi viene eseguito in un thread in background separato per evitare di bloccare in modo significativo il thread che originariamente allocava tutta la memoria.

Una volta che la memoria viene copiata dallo spazio Eden e allocata utilizzando questo schema più costoso che può liberare singoli blocchi dopo un ciclo GC iniziale, gli oggetti si spostano in una regione di memoria più persistente. Quei singoli pezzi vengono quindi liberati nei successivi cicli GC se cessano di essere referenziati.

Velocità

Quindi, in parole povere, il motivo per cui il GC Java potrebbe benissimo sovraperformare C o C ++ nell'allocazione diretta dell'heap è perché sta usando la strategia di allocazione più economica e totalmente degenerata nel thread che richiede di allocare memoria. Quindi consente di risparmiare il lavoro più costoso che normalmente dovremmo fare quando utilizziamo un allocatore più generale come quello semplice mallocper un altro thread.

Quindi concettualmente il GC in realtà deve fare complessivamente più lavoro, ma lo sta distribuendo su più thread in modo che il costo completo non sia pagato in anticipo da un singolo thread. Permette al thread di allocare memoria per farlo super economico, e quindi rinviare la vera spesa richiesta per fare le cose correttamente in modo che i singoli oggetti possano effettivamente essere liberati su un altro thread. In C o C ++ quando malloco chiamiamo operator new, dobbiamo pagare l'intero costo in anticipo all'interno dello stesso thread.

Questa è la differenza principale e il motivo per cui Java potrebbe molto meglio di C o C ++ usando solo ingenui chiamate malloco operator newallocare un gruppo di pezzi di adolescenti individualmente. Ovviamente ci saranno in genere alcune operazioni atomiche e qualche potenziale blocco quando si avvia il ciclo GC, ma è probabilmente ottimizzato abbastanza.

Fondamentalmente la semplice spiegazione si riduce al pagamento di un costo più pesante in un singolo thread ( malloc) rispetto al pagamento di un costo più economico in un singolo thread e quindi al pagamento del costo più pesante in un altro thread che può essere eseguito in parallelo ( GC). Come inconveniente, fare in questo modo implica che siano necessarie due indirette per ottenere dal riferimento all'oggetto l'oggetto come richiesto per consentire all'allocatore di copiare / spostare la memoria senza invalidare i riferimenti agli oggetti esistenti, e inoltre si può perdere la localizzazione spaziale una volta che la memoria degli oggetti è spostato dallo spazio "Eden".

Ultimo ma non meno importante, il confronto è un po 'ingiusto perché il codice C ++ normalmente non alloca un carico di oggetti barca individualmente sull'heap. Il codice C ++ decente tende ad allocare memoria per molti elementi in blocchi contigui o nello stack. Se alloca un carico di piccoli oggetti uno alla volta nel negozio gratuito, il codice è shite.


0

Dipende tutto da chi misura la velocità, quale velocità di implementazione misurano e cosa vogliono dimostrare. E cosa confrontano.

Se consideri semplicemente l'allocazione / deallocazione, in C ++ potresti avere 1.000.000 di chiamate a malloc e 1.000.000 di chiamate a free (). In Java, avresti 1.000.000 di chiamate a new () e un garbage collector in esecuzione in un ciclo che trova 1.000.000 di oggetti che può liberare. Il loop può essere più veloce della chiamata free ().

D'altra parte, malloc / free è migliorato altre volte e in genere malloc / free imposta solo un bit in una struttura di dati separata ed è ottimizzato per il funzionamento di malloc / free nello stesso thread, quindi in un ambiente multithread nessuna variabile di memoria condivisa vengono utilizzati in molti casi (e il blocco o le variabili della memoria condivisa sono molto costosi).

D'altra parte, ci sono cose come il conteggio dei riferimenti di cui potresti aver bisogno senza la raccolta dei rifiuti e che non viene fornito gratuitamente.

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.