Cosa sostiene l'affermazione secondo cui C ++ può essere più veloce di una JVM o CLR con JIT? [chiuso]


119

Un tema ricorrente su SE ho notato in molte domande è l'argomento in corso secondo cui C ++ è più veloce e / o più efficiente di linguaggi di livello superiore come Java. La contro argomentazione è che la moderna JVM o CLR può essere altrettanto efficiente grazie a JIT e così via per un numero crescente di attività e che il C ++ è sempre più efficiente se sai cosa stai facendo e perché fai le cose in un certo modo meriterà un aumento delle prestazioni. È ovvio e ha perfettamente senso.

Mi piacerebbe sapere una spiegazione di base (se esiste una cosa del genere ...) sul perché e su come determinate attività sono più veloci in C ++ rispetto a JVM o CLR? È semplicemente perché C ++ è compilato nel codice macchina mentre JVM o CLR hanno ancora il sovraccarico di elaborazione della compilazione JIT in fase di esecuzione?

Quando provo a cercare l'argomento, tutto ciò che trovo sono gli stessi argomenti che ho delineato sopra senza alcuna informazione dettagliata per capire esattamente come il C ++ può essere utilizzato per il calcolo ad alte prestazioni.


Le prestazioni dipendono anche dalla complessità del programma.
Pandu,

23
Aggiungo a "Il C ++ è sempre più efficiente se sai cosa stai facendo e perché fare le cose in un certo modo meriterà un aumento delle prestazioni". dicendo che non è solo una questione di conoscenza, è una questione di tempo per gli sviluppatori. Non è sempre efficiente massimizzare l'ottimizzazione. Questo è il motivo per cui esistono linguaggi di livello superiore come Java e Python (tra le altre ragioni) - per ridurre il tempo che un programmatore deve dedicare alla programmazione per svolgere un determinato compito a spese dell'ottimizzazione altamente sintonizzata.
Joel Cornett,

4
@Joel Cornett: sono totalmente d'accordo. Sono decisamente più produttivo in Java che in C ++ e considero C ++ solo quando ho bisogno di scrivere codice molto veloce. D'altra parte ho visto che il codice C ++ scritto male è veramente lento: il C ++ è meno utile nelle mani di programmatori non qualificati.
Giorgio,

3
Qualsiasi output di compilazione che può essere prodotto da un JIT può essere prodotto da C ++, ma il codice che C ++ può produrre potrebbe non essere necessariamente prodotto da un JIT. Quindi le capacità e le caratteristiche prestazionali del C ++ sono un superset di quelle di qualsiasi linguaggio di livello superiore. QED
tylerl

1
@Doval Tecnicamente vero, ma di regola puoi contare da un lato i possibili fattori di runtime che influenzano le prestazioni di un programma. Di solito senza usare più di due dita. Nel peggiore dei casi spedisci più binari ... tranne per il fatto che non hai nemmeno bisogno di farlo perché il potenziale speedup è trascurabile, motivo per cui nessuno si preoccupa nemmeno.
Tylerl,

Risposte:


200

Si tratta solo della memoria (non della JIT). Il "vantaggio rispetto a C" di JIT è principalmente limitato all'ottimizzazione delle chiamate virtuali o non virtuali tramite l'integrazione, cosa che il BTB della CPU sta già lavorando sodo.

Nelle macchine moderne, l'accesso alla RAM è molto lento (rispetto a tutto ciò che fa la CPU), il che significa che le applicazioni che utilizzano il più possibile le cache (che è più facile quando viene utilizzata meno memoria) possono essere fino a cento volte più veloci di quelle che non lo fanno. E ci sono molti modi in cui Java utilizza più memoria di C ++ e rende più difficile scrivere applicazioni che sfruttano appieno la cache:

  • Esiste un sovraccarico di memoria di almeno 8 byte per ogni oggetto e l'uso di oggetti anziché primitivi è richiesto o preferito in molti punti (vale a dire le raccolte standard).
  • Le stringhe sono composte da due oggetti e hanno un sovraccarico di 38 byte
  • UTF-16 viene utilizzato internamente, il che significa che ogni carattere ASCII richiede due byte anziché uno (Oracle JVM ha recentemente introdotto un'ottimizzazione per evitarlo per stringhe ASCII pure).
  • Non esiste un tipo di riferimento aggregato (ovvero le strutture) e, a sua volta, non esistono matrici di tipi di riferimento aggregati. Un oggetto Java, o array di oggetti Java, ha una localizzazione della cache L1 / L2 molto scarsa rispetto a C-struct e array.
  • I generici Java usano la cancellazione del tipo, che ha una scarsa localizzazione della cache rispetto all'istanza del tipo.
  • L'allocazione degli oggetti è opaca e deve essere eseguita separatamente per ciascun oggetto, quindi è impossibile per un'applicazione disporre deliberatamente i propri dati in modo intuitivo e tenerli come dati strutturati.

Alcuni altri fattori legati alla memoria ma non alla cache:

  • Non c'è allocazione dello stack, quindi tutti i dati non primitivi con cui lavori devono essere nell'heap e passare attraverso la garbage collection (alcuni JIT recenti eseguono l'allocazione dello stack dietro le quinte in alcuni casi).
  • Poiché non esistono tipi di riferimento aggregati, non vi è alcun passaggio di stack di tipi di riferimento aggregati. (Pensa al passaggio efficiente di argomenti Vector)
  • La garbage collection può danneggiare i contenuti della cache L1 / L2 e le pause stop-the-world di GC danneggiano l'interattività.
  • La conversione tra tipi di dati richiede sempre la copia; non è possibile prendere un puntatore a un mucchio di byte ottenuti da un socket e interpretarli come float.

Alcune di queste cose sono compromessi (non dover fare la gestione manuale della memoria vale la pena rinunciare a molte prestazioni per la maggior parte delle persone), alcune sono probabilmente il risultato del tentativo di mantenere Java semplice e alcune sono errori di progettazione (anche se forse solo con il senno di poi , ovvero UTF-16 era una codifica a lunghezza fissa al momento della creazione di Java, il che rende la decisione di sceglierla molto più comprensibile).

Vale la pena notare che molti di questi compromessi sono molto diversi per Java / JVM rispetto a quelli per C # / CIL. .NET CIL dispone di strutture di tipo di riferimento, allocazione / passaggio di stack, matrici impilate di strutture e generici istanziati dal tipo.


37
+1 - nel complesso, questa è una buona risposta. Tuttavia, non sono sicuro che il punto elenco "non c'è allocazione di stack" sia del tutto accurato. I JIT Java spesso eseguono un'analisi di escape per consentire l'allocazione dello stack ove possibile - forse ciò che dovresti dire è che il linguaggio Java non consente al programmatore di decidere quando un oggetto viene allocato in stack anziché allocato in heap. Inoltre, se è in uso un garbage collector generazionale (utilizzato da tutte le JVM moderne), "allocazione heap" significa una cosa completamente diversa (con caratteristiche di prestazione completamente diverse) rispetto a un ambiente C ++.
Daniel Pryden,

5
Penso che ci siano altre due cose, ma per lo più lavoro con cose di livello molto più alto, quindi dì se sbaglio. Non puoi davvero scrivere C ++ senza sviluppare una consapevolezza più generale di ciò che sta realmente accadendo nella memoria e di come funziona effettivamente il codice macchina mentre lo scripting o i linguaggi delle macchine virtuali sottraggono tutta quella roba dalla tua attenzione. Hai anche un controllo molto più preciso su come funzionano le cose, mentre in una macchina virtuale o in un linguaggio interpretato fai affidamento su ciò che gli autori delle librerie di base potrebbero aver ottimizzato per uno scenario eccessivamente specifico.
Erik Reppen,

18
+1. Un'altra cosa che aggiungerei (ma non sono disposto a inviare una nuova risposta per): l'indicizzazione dell'array in Java comporta sempre il controllo dei limiti. Con C e C ++, non è così.
riwalk

7
Vale la pena notare che l'allocazione dell'heap di Java è significativamente più veloce di una versione ingenua con C ++ (a causa del pooling interno e cose), ma l'allocazione della memoria in C ++ può essere significativamente migliore se sai cosa stai facendo.
Brendan Long,

10
@BrendanLong, true .. ma solo se la memoria è pulita - una volta che un'app è in esecuzione per un po ', l'allocazione della memoria sarà più lenta a causa della necessità di GC che rallenta notevolmente le cose in quanto deve liberare memoria, eseguire finalizzatori e quindi compatto. È un compromesso che avvantaggia i benchmark ma (IMHO) nel complesso rallenta le app.
gbjbaanb,

67

È semplicemente perché C ++ è compilato nel codice assembly / machine mentre Java / C # ha ancora il sovraccarico di elaborazione della compilazione JIT in fase di esecuzione?

Parzialmente, ma in generale, supponendo un compilatore JIT all'avanguardia assolutamente fantastico, il codice C ++ corretto tende ancora a funzionare meglio del codice Java per DUE motivi principali:

1) i modelli C ++ fornire migliori servizi per la scrittura di codice che è sia generica e efficiente . I modelli forniscono al programmatore C ++ un'astrazione molto utile con sovraccarico di runtime ZERO. (I modelli sono fondamentalmente la digitazione di anatre in fase di compilazione.) Al contrario, il meglio che si ottiene con i generici Java è sostanzialmente funzioni virtuali. Le funzioni virtuali hanno sempre un sovraccarico di runtime e generalmente non possono essere integrate.

In generale, la maggior parte delle lingue, tra cui Java, C # e persino C, consente di scegliere tra efficienza e generalità / astrazione. I modelli C ++ offrono entrambi (a costo di tempi di compilazione più lunghi).

2) Il fatto che lo standard C ++ non abbia molto da dire sul layout binario di un programma C ++ compilato offre ai compilatori C ++ un margine di manovra molto più ampio rispetto a un compilatore Java, consentendo ottimizzazioni migliori (a costo di maggiori difficoltà nel debug a volte. ). In effetti, la natura stessa delle specifiche del linguaggio Java impone una riduzione delle prestazioni in determinate aree. Ad esempio, non è possibile avere una matrice contigua di oggetti in Java. Puoi avere solo una matrice contigua di puntatori a oggetti(riferimenti), il che significa che l'iterazione su un array in Java comporta sempre il costo dell'indirizzamento indiretto. La semantica del valore di C ++ abilita comunque le matrici contigue. Un'altra differenza è il fatto che il C ++ consente di allocare oggetti nello stack, mentre Java no, il che significa che, in pratica, poiché la maggior parte dei programmi C ++ tende ad allocare oggetti nello stack, il costo dell'allocazione è spesso vicino allo zero.

Un'area in cui C ++ potrebbe rimanere indietro rispetto a Java è qualsiasi situazione in cui molti piccoli oggetti devono essere allocati sull'heap. In questo caso, il sistema di garbage collection di Java probabilmente porterà a prestazioni migliori rispetto allo standard newe deletein C ++ perché il GC Java consente la deallocazione di massa. Ma ancora una volta, un programmatore C ++ può compensare ciò utilizzando un pool di memoria o un allocatore di lastre, mentre un programmatore Java non può fare ricorso di fronte a un modello di allocazione della memoria per il quale il runtime Java non è ottimizzato.

Inoltre, vedi questa risposta eccellente per ulteriori informazioni su questo argomento.


6
Buona risposta, ma un punto minore: "I modelli C ++ offrono entrambi (a costo di tempi di compilazione più lunghi.)" Vorrei anche aggiungere a costo di dimensioni del programma più grandi. Potrebbe non essere sempre un problema, ma se lo sviluppo per dispositivi mobili, sicuramente può esserlo.
Leone

9
@luiscubal: no, a questo proposito, i generici C # sono molto simili a Java (in quanto viene preso lo stesso percorso di codice "generico" indipendentemente dai tipi che vengono passati.) Il trucco per i modelli C ++ è che il codice viene istanziato una volta per ogni tipo a cui è applicato. Quindi std::vector<int>è un array dinamico progettato solo per ints e il compilatore è in grado di ottimizzarlo di conseguenza. AC # List<int>è ancora solo un List.
jalf

12
@jalf C # List<int>usa un Java int[], non Object[]come Java. Vedere stackoverflow.com/questions/116988/...
luiscubal

5
@luiscubal: la tua terminologia non è chiara. Il JIT non agisce in quello che considererei "tempo di compilazione". Hai ragione, ovviamente, dato un compilatore JIT sufficientemente intelligente e aggressivo, in realtà non ci sono limiti a ciò che potrebbe fare. Ma C ++ richiede questo comportamento. Inoltre, i modelli C ++ consentono al programmatore di specificare specializzazioni esplicite, abilitando ulteriori ottimizzazioni esplicite ove applicabile. C # non ha equivalenti per questo. Ad esempio, in C ++, potrei definire un vector<N>dove, per il caso specifico di vector<4>, la mia implementazione SIMD codificata a mano dovrebbe essere usata
jalf

5
@Leo: il gonfiamento del codice attraverso i modelli è stato un problema 15 anni fa. Con una forte templatizzazione e integrazione, oltre a compilatori di abilità acquisiti da allora (come piegare istanze identiche), oggigiorno un sacco di codice si riduce attraverso i modelli.
sabato

46

Ciò che le altre risposte (6 finora) sembrano aver dimenticato di menzionare, ma ciò che considero molto importante per rispondere a questa domanda, è una delle filosofie di progettazione di base del C ++, che è stata formulata e utilizzata da Stroustrup dal primo giorno:

Non paghi per ciò che non usi.

Ci sono altri importanti principi di progettazione alla base che hanno fortemente modellato il C ++ (come quello che non dovresti essere costretto a un paradigma specifico), ma non paghi per ciò che non usi è proprio lì tra i più importanti.


Nel suo libro The Design and Evolution of C ++ (di solito indicato come [D&E]), Stroustrup descrive in primo luogo le necessità che gli hanno fatto venire in mente C ++. Nelle mie parole: per la sua tesi di dottorato (qualcosa a che fare con le simulazioni di rete, IIRC), ha implementato un sistema in SIMULA, che gli piaceva molto, perché il linguaggio era molto buono nel consentirgli di esprimere i suoi pensieri direttamente in codice. Tuttavia, il programma risultante è andato troppo lentamente, e per ottenere una laurea, ha riscritto la cosa in BCPL, un predecessore di C. Scrivere il codice in BCPL lo descrive come un dolore, ma il programma risultante è stato abbastanza veloce da consegnare risultati, che gli hanno permesso di terminare il suo dottorato di ricerca.

Successivamente, desiderava una lingua che consentisse di tradurre i problemi del mondo reale nel codice il più direttamente possibile, ma che consentisse anche al codice di essere molto efficiente.
In seguito a ciò, ha creato quello che sarebbe poi diventato C ++.


Quindi l'obiettivo sopra citato non è solo uno dei numerosi principi fondamentali di progettazione alla base, è molto vicino alla ragion d'essere per C ++. E può essere trovato praticamente ovunque nella lingua: le funzioni sono solo virtualquando lo desideri (poiché chiamare le funzioni virtuali ha un leggero sovraccarico) I POD vengono inizializzati automaticamente solo quando lo richiedi esplicitamente, le eccezioni ti costano solo le prestazioni quando effettivamente lanciarli (considerando che era un obiettivo di progettazione esplicito consentire l'installazione / pulizia degli stackframe a un prezzo molto basso), nessun GC in esecuzione ogni volta che lo si sente, ecc.

C ++ ha scelto esplicitamente di non darti alcune comodità ("devo rendere questo metodo virtuale qui?") In cambio di prestazioni ("no, non lo so, e ora il compilatore può inlinefarlo e ottimizzare il diavolo fuori dal tutto! ") e, non a caso, ciò ha comportato un aumento delle prestazioni rispetto alle lingue più convenienti.


4
Non paghi per ciò che non usi. => e poi hanno aggiunto RTTI :(
Matthieu M.

11
@Matthieu: anche se capisco il tuo sentimento, non posso fare a meno di notare che anche quello è stato aggiunto con cura per quanto riguarda le prestazioni. RTTI viene specificato in modo che possa essere implementato utilizzando le tabelle virtuali e quindi aggiunge pochissime spese generali se non lo si utilizza. Se non usi il polimorfismo, non ci sono costi. Mi sto perdendo qualcosa?
sabato

9
@Matthieu: certo, c'è ragione. Ma questa ragione è razionale? Da quello che posso vedere, il "costo di RTTI", se non utilizzato, è un ulteriore puntatore nella tabella virtuale di ogni classe polimorfica, che punta a qualche oggetto RTTI allocato staticamente da qualche parte. A meno che tu non voglia programmare il chip nel mio tostapane, come potrebbe mai essere rilevante?
sbi,

4
@Aaronaught: sono in perdita su cosa rispondere a questo. Hai davvero respinto la mia risposta perché sottolinea la filosofia che ha portato Stroustrup et al. Ad aggiungere funzionalità in un modo che consenta prestazioni, piuttosto che elencare questi modi e funzionalità individualmente?
sabato

9
@Aaronaught: hai la mia simpatia.
sabato

29

Conosci il documento di ricerca di Google su questo argomento?

Dalla conclusione:

Troviamo che per quanto riguarda le prestazioni, il C ++ vince con un ampio margine. Tuttavia, ha anche richiesto i più estesi sforzi di ottimizzazione, molti dei quali sono stati fatti a un livello di sofisticazione che non sarebbe stato disponibile per il programmatore medio.

Questa è almeno una spiegazione parziale, nel senso di "perché i compilatori C ++ del mondo reale producono codice più veloce dei compilatori Java con misure empiriche".


4
Oltre alle differenze di utilizzo della memoria e della cache, una delle più importanti è la quantità di ottimizzazione eseguita. Confronta quante ottimizzazioni GCC / LLVM (e probabilmente Visual C ++ / ICC) fanno rispetto al compilatore HotSpot Java: molto di più, soprattutto per quanto riguarda i loop, l'eliminazione di rami ridondanti e l'allocazione dei registri. I compilatori JIT di solito non hanno il tempo per queste ottimizzazioni aggressive, anche se potrebbero implementarle meglio utilizzando le informazioni di runtime disponibili.
Gratian Lup,

2
@GratianLup: mi chiedo se ciò sia (ancora) vero con LTO.
Deduplicatore,

2
@GratianLup: non dimentichiamo l'ottimizzazione guidata dal profilo per C ++ ...
Deduplicator

23

Questo non è un duplicato delle tue domande, ma la risposta accettata risponde alla maggior parte delle tue domande: una moderna revisione di Java

Per riassumere:

Fondamentalmente, la semantica di Java impone che sia un linguaggio più lento del C ++.

Quindi, a seconda di quale altra lingua si confronta C ++, si potrebbe ottenere o meno la stessa risposta.

In C ++ hai:

  • Capacità di fare allineamento intelligente,
  • generazione di codice generico con forte localizzazione (modelli)
  • i dati più piccoli e compatti possibili
  • opportunità per evitare le indicazioni indirette
  • comportamento prevedibile della memoria
  • ottimizzazioni del compilatore possibili solo a causa dell'utilizzo di astrazioni di alto livello (modelli)

Queste sono le caratteristiche o gli effetti collaterali della definizione della lingua che la rende teoricamente più efficiente in termini di memoria e velocità rispetto a qualsiasi lingua che:

  • utilizzare indirettamente in modo massiccio (linguaggi "tutto è un riferimento / puntatore gestito"): indiretto significa che la CPU deve saltare in memoria per ottenere i dati necessari, aumentando gli errori della cache della CPU, il che significa rallentare l'elaborazione - C usa anche indiretti a molto anche se può avere piccoli dati come C ++;
  • generare oggetti di grandi dimensioni a cui si accede indirettamente ai membri: questa è una conseguenza di avere riferimenti di default, i membri sono puntatori, quindi quando si ottiene un membro è possibile che non si ottengano dati vicini al nocciolo dell'oggetto genitore, provocando nuovamente errori della cache.
  • usare un collettore di grandi dimensioni: rende impossibile la prevedibilità delle prestazioni (in base alla progettazione).

L'allineamento aggressivo C ++ del compilatore riduce o elimina molte indirette. La capacità di generare un piccolo set di dati compatti lo rende compatibile con la cache se non si diffondono questi dati su tutta la memoria invece che raggruppati insieme (entrambi sono possibili, C ++ consente solo di scegliere). RAII rende prevedibile il comportamento della memoria C ++, eliminando molti problemi in caso di simulazioni in tempo reale o semi-in tempo reale, che richiedono alta velocità. I problemi di localizzazione, in generale, possono essere riassunti da questo: più piccolo è il programma / i dati, più veloce è l'esecuzione. Il C ++ offre diversi modi per assicurarsi che i tuoi dati siano dove vuoi che siano (in un pool, un array o qualsiasi altra cosa) e che siano compatti.

Ovviamente, ci sono altri linguaggi che possono fare lo stesso, ma sono meno popolari perché non forniscono tanti strumenti di astrazione come C ++, quindi sono meno utili in molti casi.


7

Si tratta principalmente di memoria (come diceva Michael Borgwardt) con l'aggiunta di un po 'di inefficienza della JIT.

Una cosa non menzionata è la cache: per utilizzare completamente la cache, è necessario disporre i dati in modo contiguo (ovvero tutti insieme). Ora con un sistema GC, la memoria viene allocata sull'heap GC, il che è rapido, ma man mano che la memoria viene utilizzata, il GC si avvia regolarmente e rimuove i blocchi non più utilizzati, quindi compatta i rimanenti insieme. Ora, a parte l'ovvia lentezza nello spostare quei blocchi usati insieme, questo significa che i dati che stai usando potrebbero non essere uniti. Se disponi di un array di 1000 elementi, a meno che tu non li abbia allocati tutti in una volta (e quindi abbia aggiornato i loro contenuti anziché eliminarli e crearne di nuovi, che verranno creati alla fine dell'heap), questi verranno sparsi in tutto l'heap, richiedendo così diversi hit di memoria per leggerli tutti nella cache della CPU. L'app AC / C ++ molto probabilmente allocerà la memoria per questi elementi e quindi aggiornerai i blocchi con i dati. (ok, ci sono strutture di dati come un elenco che si comportano più come le allocazioni di memoria del GC, ma la gente sa che queste sono più lente dei vettori).

Puoi vederlo in funzione semplicemente sostituendo qualsiasi oggetto StringBuilder con String ... Stringbuilders funziona pre-allocando memoria e riempiendola, ed è un noto trucco prestazionale per i sistemi java / .NET.

Non dimenticare che il paradigma "elimina vecchie e alloca nuove copie" è molto utilizzato in Java / C #, semplicemente perché viene detto alle persone che le allocazioni di memoria sono davvero veloci a causa del GC, e quindi il modello di memoria diffusa viene utilizzato ovunque ( tranne che per i costruttori di stringhe, ovviamente), quindi tutte le vostre librerie tendono ad essere sprecate di memoria e ne usano molto, nessuna delle quali ottiene il vantaggio della contiguità. Dai la colpa al clamore di GC per questo: ti hanno detto che la memoria era libera, lol.

Il GC stesso è ovviamente un altro colpo perfetto: quando gira, non solo deve spazzare l'heap, ma deve anche liberare tutti i blocchi inutilizzati, e quindi deve eseguire tutti i finalizzatori (sebbene questo fosse fatto separatamente il la prossima volta che l'app viene interrotta) (non so se è ancora un tale colpo perfetto, ma tutti i documenti che leggo dicono di usare solo i finalizzatori se davvero necessari) e quindi deve spostare quei blocchi in posizione in modo che l'heap sia compattato e aggiorna il riferimento alla nuova posizione del blocco. Come puoi vedere, è un sacco di lavoro!

I risultati perfetti per la memoria C ++ si riducono alle allocazioni di memoria: quando hai bisogno di un nuovo blocco, devi camminare nell'heap alla ricerca del prossimo spazio libero abbastanza grande, con un heap fortemente frammentato, non è così veloce come un GC 'basta allocare un altro blocco alla fine' ma penso che non sia così lento come tutto il lavoro svolto dalla compattazione GC, e può essere mitigato usando più blocchi di dimensioni fisse (altrimenti noti come pool di memoria).

C'è di più ... come caricare gli assembly dal GAC che richiedono controlli di sicurezza, percorsi dei probe ( accendi sxstrace e guarda cosa sta facendo!) E altri overingegneria generale che sembrano essere molto più popolari con java / .net di C / C ++.


2
Molte cose che scrivi non sono vere per i moderni generatori di rifiuti generazionali.
Michael Borgwardt,

3
@MichaelBorgwardt come? Dico "GC funziona regolarmente" e "compatta l'heap". Il resto della mia risposta riguarda il modo in cui le strutture di dati dell'applicazione usano la memoria.
gbjbaanb,

6

"È semplicemente perché C ++ è compilato in codice assembly / machine mentre Java / C # ha ancora il sovraccarico di elaborazione della compilazione JIT in fase di esecuzione?" Fondamentalmente sì!

Nota rapida, tuttavia, Java ha più costi generali della semplice compilazione JIT. Ad esempio, fa molto più controllo per te (che è come fa le cose come ArrayIndexOutOfBoundsExceptionse NullPointerExceptions). Il garbage collector è un altro sovraccarico significativo.

C'è un confronto abbastanza dettagliato qui .


2

Tenere presente che quanto segue sta solo confrontando la differenza tra la compilazione nativa e JIT e non copre le specifiche di alcun linguaggio o framework specifici. Potrebbero esserci motivi legittimi per scegliere una piattaforma particolare oltre a questa.

Quando affermiamo che il codice nativo è più veloce, stiamo parlando del tipico caso d' uso del codice compilato in modo nativo rispetto al codice compilato JIT, in cui l'uso tipico di un'applicazione compilata JIT deve essere eseguito dall'utente, con risultati immediati (ad esempio, no prima in attesa sul compilatore). In tal caso, non credo che nessuno possa affermare con una faccia seria che il codice compilato JIT può corrispondere o battere il codice nativo.

Supponiamo di avere un programma scritto in un linguaggio X, e possiamo compilarlo con un compilatore nativo e di nuovo con un compilatore JIT. Ogni flusso di lavoro ha le stesse fasi coinvolte, che possono essere generalizzate come (Codice -> Rappresentanza intermedia -> Codice macchina -> Esecuzione). La grande differenza tra due è quali fasi sono visualizzate dall'utente e quali sono viste dal programmatore. Con la compilazione nativa, il programmatore vede tutto tranne la fase di esecuzione, ma con la soluzione JIT, la compilazione in codice macchina è vista dall'utente, oltre all'esecuzione.

L'affermazione che A è più veloce di B si riferisce al tempo impiegato per l'esecuzione del programma, come visto dall'utente . Se assumiamo che entrambi i pezzi di codice funzionino in modo identico nella fase di esecuzione, dobbiamo supporre che il flusso di lavoro JIT sia più lento per l'utente, poiché deve anche vedere il tempo T della compilazione rispetto al codice macchina, dove T> 0. Quindi , affinché qualsiasi possibilità che il flusso di lavoro JIT esegua lo stesso del flusso di lavoro nativo, per l'utente, è necessario ridurre i tempi di esecuzione del codice, in modo tale che Esecuzione + Compilazione al codice macchina siano inferiori alla sola fase di esecuzione del flusso di lavoro nativo. Ciò significa che dobbiamo ottimizzare il codice meglio nella compilazione JIT che nella compilazione nativa.

Ciò, tuttavia, è piuttosto impossibile, poiché per eseguire le ottimizzazioni necessarie per accelerare l'esecuzione, dobbiamo dedicare più tempo nella fase di compilazione per la fase del codice macchina e, quindi, ogni volta che risparmiamo a causa del codice ottimizzato viene effettivamente perso, poiché lo aggiungiamo alla compilation. In altre parole, la "lentezza" di una soluzione basata su JIT non è semplicemente dovuta al tempo aggiunto per la compilazione JIT, ma il codice prodotto da quella compilazione esegue più lentamente di una soluzione nativa.

Userò un esempio: registro allocazione. Poiché l'accesso alla memoria è migliaia di volte più lento dell'accesso ai registri, idealmente vogliamo usare i registri laddove possibile e avere il minor numero di accessi di memoria possibile, ma abbiamo un numero limitato di registri e dobbiamo trasferire lo stato in memoria quando ne abbiamo bisogno un registro. Se utilizziamo un algoritmo di allocazione dei registri che richiede 200 ms per il calcolo e, di conseguenza, risparmiamo 2 ms di tempo di esecuzione - non stiamo sfruttando al meglio il tempo per un compilatore JIT. Soluzioni come l'algoritmo di Chaitin, che può produrre codice altamente ottimizzato, non sono adatte.

Il ruolo del compilatore JIT è quello di trovare il miglior equilibrio tra tempo di compilazione e qualità del codice prodotto, tuttavia, con una grande propensione per i tempi di compilazione rapidi, poiché non si desidera lasciare l'utente in attesa. Le prestazioni del codice in esecuzione sono più lente nel caso JIT, poiché il compilatore nativo non è vincolato (molto) dal tempo nell'ottimizzazione del codice, quindi è libero di usare i migliori algoritmi. La possibilità che la compilazione + esecuzione complessiva per un compilatore JIT possa battere solo il tempo di esecuzione per il codice compilato in modo nativo è effettivamente 0.

Ma le nostre VM non si limitano solo alla compilazione JIT. Impiegano tecniche di compilazione anticipate, memorizzazione nella cache, hot swap e ottimizzazioni adattive. Quindi modifichiamo la nostra affermazione che le prestazioni sono ciò che l'utente vede e limitiamole al tempo impiegato per l'esecuzione del programma (supponiamo che abbiamo compilato AOT). Possiamo effettivamente rendere il codice di esecuzione equivalente al compilatore nativo (o forse meglio?). Una grande richiesta per le macchine virtuali è che potrebbero essere in grado di produrre un codice di qualità migliore rispetto a un compilatore nativo, perché ha accesso a più informazioni - quella del processo in esecuzione, come la frequenza con cui una determinata funzione può essere eseguita. La VM può quindi applicare ottimizzazioni adattive al codice più essenziale tramite hot swap.

Tuttavia, c'è un problema con questo argomento: si presume che l'ottimizzazione guidata dal profilo e simili sia qualcosa di unico per le macchine virtuali, il che non è vero. Possiamo applicarlo anche alla compilazione nativa - compilando la nostra applicazione con la profilazione abilitata, registrando le informazioni e quindi ricompilando l'applicazione con quel profilo. Probabilmente vale anche la pena sottolineare che lo scambio di codice non è qualcosa che solo un compilatore JIT può fare, possiamo farlo per il codice nativo, anche se le soluzioni basate su JIT per farlo sono più prontamente disponibili e molto più facili per lo sviluppatore. Quindi la grande domanda è: può una VM fornirci alcune informazioni che la compilazione nativa non può, il che può migliorare le prestazioni del nostro codice?

Non riesco a vederlo da solo. Possiamo applicare la maggior parte delle tecniche di una tipica VM anche al codice nativo, sebbene il processo sia maggiormente coinvolto. Allo stesso modo, possiamo applicare eventuali ottimizzazioni di un compilatore nativo a una macchina virtuale che utilizza la compilazione AOT o le ottimizzazioni adattative. La realtà è che la differenza tra il codice eseguito in modo nativo e quello eseguito in una VM non è così grande come ci è stato fatto credere. Alla fine portano allo stesso risultato, ma adottano un approccio diverso per arrivarci. La VM utilizza un approccio iterativo per produrre codice ottimizzato, in cui il compilatore nativo lo prevede dall'inizio (e può essere migliorato con un approccio iterativo).

Un programmatore C ++ potrebbe obiettare che ha bisogno delle ottimizzazioni fin dall'inizio, e non dovrebbe essere in attesa di una macchina virtuale per capire come eseguirle, se non del tutto. Questo è probabilmente un punto valido con la nostra attuale tecnologia, poiché l'attuale livello di ottimizzazione nelle nostre macchine virtuali è inferiore a quello che i compilatori nativi possono offrire, ma ciò potrebbe non essere sempre il caso se le soluzioni AOT nelle nostre macchine virtuali migliorano, ecc.


0

Questo articolo è un riepilogo di una serie di post di blog che cercano di confrontare la velocità di c ++ vs c # e i problemi che devi superare in entrambe le lingue per ottenere codice ad alte prestazioni. Il sommario è "la tua biblioteca conta molto più di ogni altra cosa, ma se sei in c ++ puoi superarla". o "le lingue moderne hanno librerie migliori e ottengono così risultati più rapidi con uno sforzo minore" a seconda della tua inclinazione filosofica.


0

Penso che la vera domanda qui non sia "quale è più veloce?" ma "quale ha il miglior potenziale per prestazioni più elevate?". Visto in questi termini, C ++ vince chiaramente: è compilato in codice nativo, non c'è JITting, è un livello inferiore di astrazione, ecc.

È lontano dalla storia completa.

Poiché il C ++ viene compilato, eventuali ottimizzazioni del compilatore devono essere eseguite al momento della compilazione e le ottimizzazioni del compilatore appropriate per una macchina potrebbero essere completamente errate per un'altra. È anche il caso che qualsiasi ottimizzazione globale del compilatore possa e favorirà determinati algoritmi o schemi di codice rispetto ad altri.

D'altra parte, un programma JITted ottimizzerà al momento JIT, quindi può fare alcuni trucchi che un programma precompilato non può e può fare ottimizzazioni molto specifiche per la macchina su cui è effettivamente in esecuzione e il codice su cui è effettivamente in esecuzione. Una volta superato il sovraccarico iniziale della JIT, in alcuni casi ha il potenziale per essere più veloce.

In entrambi i casi un'implementazione ragionevole dell'algoritmo e altre istanze del programmatore che non sono stupide saranno probabilmente fattori molto più significativi, tuttavia - ad esempio, è perfettamente possibile scrivere un codice stringa completamente cerebrale in C ++ che sarà compromesso anche da un linguaggio di scripting interpretato.


3
"Le ottimizzazioni del compilatore che sono appropriate per una macchina possono essere completamente sbagliate per un'altra" Beh, non è proprio colpa della lingua. Il codice veramente critico per le prestazioni può essere compilato separatamente per ogni macchina su cui verrà eseguito, il che è un gioco da ragazzi se compilate localmente da source ( -march=native). - "è un livello inferiore di astrazione" non è proprio vero. Il C ++ usa astrazioni di alto livello come Java (o, di fatto, quelle più alte: programmazione funzionale? Metaprogrammazione dei template?), Implementa le astrazioni in modo meno "pulito" di Java.
lasciato il

"Il codice veramente critico per le prestazioni può essere compilato separatamente per ogni macchina su cui verrà eseguito, il che è un gioco da ragazzi se si compila localmente dalla fonte" - questo non riesce a causa del presupposto che l'utente finale è anche un programmatore.
Maximus Minimus,

Non necessariamente l'utente finale, ma solo la persona responsabile dell'installazione del programma. Sul desktop e sui dispositivi mobili, in genere è l'utente finale, ma queste non sono le uniche applicazioni disponibili, certamente non le più critiche per le prestazioni. E non hai davvero bisogno di essere un programmatore per costruire un programma dalla fonte, se ha scritto correttamente gli script di costruzione come fanno tutti i buoni progetti di software libero / aperto.
lasciato circa il

1
Mentre in teoria sì, un JIT può fare più trucchi di un compilatore statico, in pratica (almeno per .NET, non conosco anche java), in realtà non lo fa. Di recente ho fatto un sacco di disassemblaggi di codice .NET JIT, e ci sono tutti i tipi di ottimizzazioni come il sollevamento del codice dagli anelli, l'eliminazione del codice morto, ecc., Che semplicemente JIT .NET non fa. Vorrei che lo fosse, ma ehi, il team di Windows all'interno di Microsoft ha cercato di uccidere .NET per anni, quindi non trattengo il respiro
Orion Edwards,

-1

La compilazione JIT ha effettivamente un impatto negativo sulle prestazioni. Se si progetta un compilatore "perfetto" e un compilatore JIT "perfetto", la prima opzione vincerà sempre in termini di prestazioni.

Sia Java che C # vengono interpretati in linguaggi intermedi e quindi compilati in codice nativo in fase di esecuzione, il che riduce le prestazioni.

Ma ora la differenza non è così ovvia per C #: Microsoft CLR produce codice nativo diverso per CPU diverse, rendendo così il codice più efficiente per la macchina su cui è in esecuzione, che non viene sempre eseguito dai compilatori C ++.

PS C # è scritto in modo molto efficace e non ha molti livelli di astrazione. Questo non è vero per Java, che non è così efficiente. Quindi, in questo caso, con il suo CLR greate, i programmi C # mostrano spesso prestazioni migliori rispetto ai programmi C ++. Per ulteriori informazioni su .Net e CLR dai un'occhiata a "CLR via C #" di Jeffrey Richter .


8
Se JIT avesse effettivamente un impatto negativo sulle prestazioni, sicuramente non verrebbe utilizzato?
Zavior,

2
@Zavior - Non riesco a pensare a una buona risposta alla tua domanda, ma non vedo come JIT non possa aggiungere sovraccarico di prestazioni extra - la JIT è un processo aggiuntivo da completare in fase di esecuzione che richiede risorse che non sono " essere speso per l'esecuzione del programma stesso, mentre un linguaggio completamente compilato è "pronto all'uso".
Anonimo

3
JIT ha un effetto positivo sulle prestazioni, non negativo, se lo metti nel contesto - Sta compilando il codice byte nel codice macchina prima di eseguirlo. I risultati possono anche essere memorizzati nella cache, consentendogli di eseguire più velocemente di un codice byte equivalente interpretato.
Casey Kuball,

3
JIT (o meglio, l'approccio bytecode) non viene utilizzato per le prestazioni, ma per comodità. Invece di precompilare i binari per ciascuna piattaforma (o un sottoinsieme comune, che non è ottimale per ognuno di essi), si compila solo a metà strada e si lascia che il compilatore JIT faccia il resto. 'Scrivi una volta, distribuisci ovunque' è il motivo per cui è fatto in questo modo. La convenienza si può avere solo con un interprete di bytecode, ma JIT non renderlo più veloce di quanto l'interprete prima (anche se non necessariamente abbastanza veloce per battere una soluzione pre-compilato; compilazione JIT fa prendere tempo, e il risultato non sempre costituiscono per questo).
martedì

4
@Tdammmers, in realtà esiste anche un componente prestazionale. Vedi java.sun.com/products/hotspot/whitepaper.html . Le ottimizzazioni possono includere elementi come gli aggiustamenti dinamici per migliorare la previsione dei rami e gli hit della cache, l'inserimento dinamico, la des-virtualizzazione, la disabilitazione del controllo dei limiti e lo srotolamento dei cicli. L'affermazione è che in molti casi questi possono più che pagare il costo di JIT.
Charles E. Grant,
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.