Un linguaggio dinamico come Ruby / Python può raggiungere prestazioni simili a quelle di C / C ++?


64

Mi chiedo se è possibile creare compilatori per linguaggi dinamici come Ruby per avere prestazioni simili e comparabili a C / C ++? Da quello che ho capito sui compilatori, prendi Ruby per esempio, la compilazione del codice Ruby non può mai essere efficiente perché il modo in cui Ruby gestisce la riflessione, caratteristiche come la conversione automatica del tipo da intero a grande intero e la mancanza di tipizzazione statica rende la costruzione di un compilatore efficiente per Ruby estremamente difficile.

È possibile creare un compilatore in grado di compilare Ruby o altri linguaggi dinamici in un binario che esegue molto da vicino a C / C ++? C'è un motivo fondamentale per cui i compilatori JIT, come PyPy / Rubinius, alla fine o non corrisponderanno mai al C / C ++ nelle prestazioni?

Nota: capisco che "performance" può essere vaga, quindi per chiarire ciò, intendevo dire, se puoi fare X in C / C ++ con performance Y, puoi fare X in Ruby / Python con prestazioni vicine a Y? Dove X è tutto, dai driver di dispositivo e codice OS, alle applicazioni web.


1
Puoi riformulare la domanda in modo che incoraggi le risposte supportate da prove adeguate sugli altri?
Raffaello

@Raphael Sono andato avanti e modificato. Penso che la mia modifica non cambi sostanzialmente il significato della domanda, ma la rende meno invitante delle opinioni.
Gilles 'SO- smetti di essere malvagio' il

1
In particolare, penso che sia necessario correggere una (o poche) misure concrete delle prestazioni. Runtime? Utilizzo dello spazio? Consumo di energia? Tempo degli sviluppatori? Ritorno dell'investimento? Nota anche questa domanda sul nostro meta che riguarda questa domanda (o meglio le sue risposte).
Raffaello

Questa domanda è un tipico iniziatore delle guerre di religione. E come possiamo vedere dalle risposte, ne stiamo avendo uno, anche se molto civile.
Andrej Bauer,

Esistono lingue dinamiche che consentono annotazioni di tipo opzionali (ad esempio: Clojure). Da quello che so, le prestazioni associate alle funzioni con annotazioni di tipo sono equivalenti a quando una lingua verrebbe digitata staticamente.
Pedro Morte Rolo,

Risposte:


68

A tutti coloro che hanno detto "sì" offrirò un contrappunto secondo cui la risposta è "no", in base alla progettazione . Tali lingue non saranno mai in grado di eguagliare le prestazioni delle lingue compilate staticamente.

Kos ha offerto il punto (molto valido) che le lingue dinamiche hanno più informazioni sul sistema in fase di esecuzione che possono essere utilizzate per ottimizzare il codice.

Tuttavia, c'è un altro lato della medaglia: è necessario tenere traccia di queste informazioni aggiuntive. Sulle architetture moderne, questo è un killer delle prestazioni.

William Edwards offre una bella panoramica dell'argomento .

In particolare, le ottimizzazioni menzionate da Kos non possono essere applicate al di là di un ambito molto limitato se non si limita il potere espressivo delle proprie lingue in modo abbastanza drastico, come indicato da Devin. Questo è ovviamente un compromesso praticabile, ma per il bene della discussione, si finisce con un linguaggio statico , non dinamico. Queste lingue differiscono fondamentalmente da Python o Ruby come la maggior parte delle persone le capirebbe.

William cita alcune interessanti diapositive IBM :

  • È possibile digitare dinamicamente ogni variabile: è necessario un controllo del tipo
  • Ogni istruzione può potenzialmente generare eccezioni a causa della mancata corrispondenza del tipo e così via: sono necessari controlli delle eccezioni
  • Ogni campo e simbolo possono essere aggiunti, eliminati e modificati in fase di esecuzione: sono necessari controlli di accesso
  • Il tipo di ogni oggetto e la sua gerarchia di classi possono essere modificati in fase di esecuzione: sono necessari controlli della gerarchia di classi

Alcuni di questi controlli possono essere eliminati dopo l'analisi (NB: questa analisi richiede anche tempo - in fase di esecuzione).

Inoltre, Kos sostiene che i linguaggi dinamici potrebbero persino superare le prestazioni del C ++. La SIC può effettivamente analizzare il comportamento del programma e applicare opportune ottimizzazioni.

Ma i compilatori C ++ possono fare lo stesso! I compilatori moderni offrono la cosiddetta ottimizzazione guidata dal profilo che, se ricevono input adeguati, può modellare il comportamento del runtime del programma e applicare le stesse ottimizzazioni che si applicherebbe un JIT.

Naturalmente, tutto questo dipende dall'esistenza di dati di allenamento realistici e, inoltre, il programma non può adattare le sue caratteristiche di runtime se il modello di utilizzo cambia a metà corsa. I JIT possono teoricamente gestirlo. Sarei interessato a vedere come questo costa in pratica, poiché, al fine di cambiare ottimizzazione, la SIC dovrebbe continuamente raccogliere dati di utilizzo che rallentano ancora una volta l'esecuzione.

In sintesi, non sono convinto che le ottimizzazioni di hot-spot di runtime superino le spese generali di monitoraggio delle informazioni di runtime a lungo termine , rispetto all'analisi statica e all'ottimizzazione.


2
@Raphael Questo è un "difetto" del compilatore allora. In particolare, l' javacottimizzazione è mai stata guidata dal profilo? Non per quanto ne sono a conoscenza. In generale, non ha senso fare in modo che il compilatore di una lingua JIT sia ottimizzato poiché la JIT è in grado di gestirla (e quantomeno, in questo modo più lingue traggono profitto dallo sforzo). Quindi (comprensibilmente) non ci sono mai stati molti sforzi javacnell'ottimizzatore, per quanto ne so (per i linguaggi .NET questo è sicuramente vero).
Konrad Rudolph,

1
@Raphael La parola chiave è: forse. Non lo mostra in nessun modo. Questo è tutto ciò che volevo dire. Ho fornito ragioni (ma nessuna prova) per la mia ipotesi nei paragrafi precedenti.
Konrad Rudolph,

1
@ Ben non nego che sia complicato. Questa è solo un'intuizione. Il monitoraggio di tutte quelle informazioni in fase di esecuzione ha un costo, dopo tutto. Non sono convinto dal tuo punto sull'IO. Se questo è prevedibile (= caso d'uso tipico), allora PGO può prevederlo. Se è falso, non sono convinto che neanche la SIC potrebbe ottimizzarla. Forse una volta ogni tanto, per pura fortuna. Ma in modo affidabile? ...
Konrad Rudolph,

2
@Konrad: il tuo intuito è falso. Non si tratta di variare in fase di esecuzione, si tratta di imprevedibilità in fase di compilazione . Il punto debole per JIT vs ottimizzazione statica non è quando il comportamento del programma cambia in fase di esecuzione "troppo veloce" per la profilazione, è quando il comportamento del programma è facile da ottimizzare in ogni singola esecuzione del programma, ma varia selvaggiamente tra piste. Un ottimizzatore statico dovrà generalmente ottimizzare solo per un set di condizioni, mentre un JIT ottimizza ciascuna corsa separatamente, per le condizioni che si verificano in quella corsa.
Ben

3
Avviso: questo thread di commenti si sta trasformando in una mini chat room. Se desideri continuare questa discussione, portala in chat. I commenti dovrebbero essere usati per migliorare il post originale. Si prega di interrompere questa conversazione qui. Grazie.
Robert Cartaino

20

se puoi fare X in C / C ++ con prestazioni Y, puoi fare X in Ruby / Python con prestazioni vicine a Y?

Sì. Prendi, ad esempio, PyPy. È una raccolta di codice Python che si comporta vicino a C nel fare interpretazione (non tanto vicino, ma neanche così lontano). Lo fa eseguendo l'analisi del programma completo sul codice sorgente per assegnare a ciascuna variabile un tipo statico (vedere i documenti Annotator e Rtyper per i dettagli), quindi, una volta armato con le stesse informazioni di tipo fornite a C, può eseguire lo stesso tipi di ottimizzazioni. Almeno in teoria.

Il compromesso ovviamente è che solo un sottoinsieme di codice Python è accettato da RPython, e in generale, anche se tale limitazione viene eliminata, solo un sottoinsieme di codice Python può fare bene: il sottoinsieme che può essere analizzato e dato tipi statici.

Se si restringe abbastanza Python, è possibile creare ottimizzatori in grado di sfruttare il sottoinsieme limitato e compilarlo in codice efficiente. Questo non è davvero un vantaggio interessante, infatti, è ben noto. Ma il punto centrale dell'utilizzo di Python (o Ruby) era innanzitutto che volevamo usare funzionalità interessanti che forse non analizzavano bene e che davano buoni risultati! Quindi la domanda interessante è in realtà ...

Inoltre, i compilatori JIT, come PyPy / Rubinius, corrisponderanno mai alle prestazioni di C / C ++?

Nah.

Con questo intendo: certo, forse man mano che il codice si accumula puoi ottenere abbastanza informazioni di battitura e abbastanza hotspot per compilare tutto il codice fino al codice macchina. E forse possiamo ottenere che funzioni meglio di C per un po 'di codice. Non penso che sia estremamente controverso. Ma deve ancora "riscaldarsi" e le prestazioni sono ancora un po 'meno prevedibili e non saranno buone come C o C ++ per alcune attività che richiedono prestazioni costantemente e prevedibilmente elevate.

I dati sulle prestazioni esistenti per Java, che contengono sia più informazioni sul tipo di Python o Ruby, sia un compilatore JIT meglio sviluppato di Python o Ruby, non corrispondono ancora a C / C ++. È, tuttavia, nello stesso campo da baseball.


1
"Il compromesso ovviamente è che solo un sottoinsieme di codice Python è accettato, o meglio, solo un sottoinsieme di codice Python può fare bene: il sottoinsieme che può essere analizzato e dato tipi statici." Questo non è abbastanza preciso. Solo il sottoinsieme può essere accettato dal compilatore RPython. La compilazione in RPython in C ragionevolmente efficiente funziona solo perché le parti difficili da compilare di Python non si verificano mai nei programmi RPython; non è solo che se si verificano non saranno ottimizzati. È l'interprete compilato che gestisce tutto Python.
Ben

1
Informazioni su JIT: ho visto più di un punto di riferimento in cui Java ha superato i gusti di C (++). Solo il C ++ con boost sembra essere affidabile. In tal caso, mi chiedo delle prestazioni per tempo degli sviluppatori, ma questo è un altro argomento.
Raffaello

@Ben: una volta che hai RPython, è banale creare un compilatore / interprete che ricorre all'utilizzo dell'interprete CPython quando il compilatore RPython fallisce, quindi "solo un sottoinsieme del codice Python può fare bene: ..." è totalmente accurato.
Lie Ryan,

9
@Raphael È stato dimostrato più volte che il codice C ++ ben scritto supera Java. È la parte "ben scritta" che è un po 'più difficile da ottenere in C ++, quindi in molti banchi si vedono i risultati che Java supera C ++. Il C ++ è quindi più costoso, ma quando sono necessari lo stretto controllo dei mem e la grana del metallo, C / C ++ è ciò a cui ti rivolgi. C in particolare è solo un assemblatore ac / p.
TC1

7
Confrontare le prestazioni massime di altre lingue con lingue come C / C ++ è una sorta di esercizio di futilità, poiché è possibile incorporare l'assemblaggio direttamente come parte delle specifiche della lingua. Qualunque cosa che la macchina potrebbe fare eseguendo un programma scritto in qualsiasi lingua, nella peggiore delle ipotesi è possibile duplicare scrivendo assembly da una traccia di istruzioni eseguite. Una metrica molto più interessante sarebbe, come suggerisce @Raphael in un commento precedente, prestazioni per sforzo di sviluppo (ore di lavoro, righe di codice, ecc.).
Patrick87,

18

La risposta breve è: non lo sappiamo , chiediamo di nuovo tra 100 anni. (Potremmo ancora non saperlo allora; forse non lo sapremo mai.)

In teoria, questo è possibile. Prendi tutti i programmi che sono mai stati scritti, traducili manualmente nel codice macchina più efficiente possibile e scrivi un interprete che associ i codici sorgente ai codici macchina. Ciò è possibile poiché è stato scritto solo un numero limitato di programmi (e man mano che vengono scritti più programmi, mantenere le traduzioni manuali). Anche questo è ovviamente completamente idiota in termini pratici.

Inoltre, in teoria, linguaggi di alto livello potrebbero essere in grado di raggiungere le prestazioni del codice macchina, ma non lo supereranno. Questo è ancora molto teorico, perché in termini pratici, molto raramente ricorriamo alla scrittura di codice macchina. Questo argomento non si applica al confronto tra linguaggi di livello superiore: non implica che C debba essere più efficiente di Python, solo che il codice macchina non può fare peggio di Python.

Venendo dall'altra parte, in termini puramente sperimentali, possiamo vedere che la maggior parte delle volte , le lingue di alto livello interpretate hanno prestazioni peggiori rispetto alle lingue di basso livello compilate. Tendiamo a scrivere codice non sensibile al tempo in linguaggi di altissimo livello e loop interni critici nel tempo, con linguaggi come C e Python in mezzo. Anche se non ho statistiche a sostegno di questo, penso che questa sia davvero la decisione migliore nella maggior parte dei casi.

Tuttavia, ci sono casi non contestati in cui linguaggi di alto livello battono il codice che si potrebbe realisticamente scrivere: ambienti di programmazione per scopi speciali. Programmi come Matlab e Mathematica sono spesso molto più bravi a risolvere alcuni tipi di problemi matematici rispetto a ciò che i semplici mortali possono scrivere. Le funzioni della biblioteca potrebbero essere state scritte in C o C ++ (che è il carburante verso il campo "le lingue di basso livello sono più efficienti"), ma non è affar mio se sto scrivendo il codice Mathematica, la biblioteca è una scatola nera.

È teoricamente possibile che Python si avvicini, o forse sia ancora più vicino, a prestazioni ottimali rispetto a C? Come visto sopra, sì, ma oggi siamo molto lontani da quello. Ancora una volta, i compilatori hanno fatto molti progressi negli ultimi decenni e questi progressi non stanno rallentando.

Le lingue di alto livello tendono a rendere più automatiche le cose, quindi hanno più lavoro da svolgere e quindi tendono ad essere meno efficienti. D'altra parte, tendono ad avere più informazioni semantiche, quindi può essere più facile individuare ottimizzazioni (se stai scrivendo un compilatore Haskell, non devi preoccuparti che un altro thread modificherà una variabile sotto il tuo naso). Uno dei numerosi sforzi per confrontare i diversi linguaggi di programmazione di mele e arance è il Computer Language Benchmark Game (precedentemente noto come shootout). Fortran tende a brillare in compiti numerici; ma quando si tratta di manipolare dati strutturati o commutazione di thread ad alta velocità, F # e Scala fanno bene. Non prendere questi risultati come vangelo: molto di ciò che stanno misurando è quanto è stato bravo l'autore del programma di test in ogni lingua.

Un argomento a favore di linguaggi di alto livello è che le prestazioni sui sistemi moderni non sono così fortemente correlate al numero di istruzioni eseguite, e meno nel tempo. Le lingue di basso livello sono buone corrispondenze per semplici macchine sequenziali. Se un linguaggio di alto livello esegue il doppio delle istruzioni, ma riesce a utilizzare la cache in modo più intelligente, quindi fa la metà del numero di errori nella cache, potrebbe finire per vincere.

Sulle piattaforme server e desktop, le CPU hanno quasi raggiunto un plateau dove non ottengono più velocemente (anche le piattaforme mobili ci stanno arrivando); ciò favorisce le lingue in cui il parallelismo è facile da sfruttare. Molti processori trascorrono la maggior parte del loro tempo in attesa di una risposta I / O; il tempo impiegato nel calcolo conta poco rispetto alla quantità di I / O e un linguaggio che consente al programmatore di ridurre al minimo le comunicazioni è un vantaggio.

Tutto sommato, mentre le lingue di alto livello iniziano con una penalità, hanno più margini di miglioramento. Quanto possono avvicinarsi? Chiedi di nuovo tra 100 anni.

Nota finale: spesso il confronto non è tra il programma più efficiente che può essere scritto nella lingua A e lo stesso nella lingua B, né tra il programma più efficiente mai scritto in ciascuna lingua, ma tra il programma più efficiente che può essere scritto da un essere umano in un certo periodo di tempo in ogni lingua. Ciò introduce un elemento che non può essere analizzato matematicamente, neppure in linea di principio. In termini pratici, ciò spesso significa che la migliore prestazione è un compromesso tra quanto codice di basso livello devi scrivere per raggiungere gli obiettivi di prestazione e quanto codice di basso livello hai tempo per scrivere per rispettare le date di rilascio.


Penso che la distinzione tra alto e basso livello sia sbagliata. Il C ++ può essere (molto) di alto livello. Tuttavia, il C ++ moderno e di alto livello non ha (necessariamente) prestazioni peggiori di un equivalente di basso livello, al contrario. C ++ e le sue librerie sono state attentamente progettate per offrire astrazioni di alto livello senza penalizzare le prestazioni. Lo stesso vale per il tuo esempio Haskell: le sue astrazioni di alto livello spesso consentono piuttosto che prevenire ottimizzazioni. La distinzione originale tra linguaggi dinamici e linguaggi statici ha più senso al riguardo.
Konrad Rudolph,

@KonradRudolph Hai ragione, in quanto basso / alto livello è una distinzione in qualche modo arbitraria. Ma anche i linguaggi dinamici e statici non catturano tutto; un JIT può eliminare gran parte della differenza. In sostanza, le risposte teoriche conosciute a questa domanda sono banali e inutili, e la risposta pratica è "dipende".
Gilles 'SO- smetti di essere malvagio' il

Bene, quindi, penso che la domanda diventi solo "quanto possono diventare bravi i JIT, e se superano la compilazione statica, possono trarne profitto anche i linguaggi compilati staticamente?" Almeno è così che capisco la domanda quando prendiamo in considerazione i JIT. E sì, sono d'accordo con la tua valutazione, ma sicuramente possiamo ottenere alcune ipotesi informate che vanno oltre "dipende". ;-)
Konrad Rudolph,

@KonradRudolph Se volessi indovinare, chiederei su Software Engineering .
Gilles 'SO-smetti di essere malvagio' il

1
La sparatoria linguistica è purtroppo una fonte discutibile di benchmark quantitativi: non accettano tutti i programmi solo quelli ritenuti tipici della lingua. Questo è un requisito difficile e molto soggettivo; significa che non si può presumere che un'implementazione di sparatorie sia effettivamente un bene (e in pratica, alcune implementazioni hanno ovviamente rifiutate alternative superiori). Il rovescio della medaglia; questi sono microbenchmark e alcune implementazioni usano tecniche insolite che non avresti mai considerato in uno scenario più normale solo per vincere le prestazioni. Quindi: è un gioco, non un'origine dati molto affidabile.
Eamon Nerbonne,

10

La differenza fondamentale tra l'istruzione C ++ x = a + be la dichiarazione di Python x = a + bè che un compilatore C / C ++ può dire da questa affermazione (e qualche informazione in più che ha prontamente disponibile sui tipi di x, ae b) esattamente ciò che il codice macchina deve essere eseguito . Considerando che per dire quali operazioni eseguirà l'istruzione Python, è necessario risolvere il problema di Halting.

In C tale istruzione verrà sostanzialmente compilata in uno dei pochi tipi di addizione macchina (e il compilatore C sa quale). In C ++ potrebbe essere compilato in quel modo, oppure potrebbe essere compilato per chiamare una funzione staticamente nota, o (nel peggiore dei casi) potrebbe dover compilare una ricerca e una chiamata di metodo virtuale, ma anche questo ha un overhead di codice macchina abbastanza piccolo. Ancora più importante, tuttavia, il compilatore C ++ può dire dai tipi staticamente noti coinvolti se può emettere una singola operazione di aggiunta rapida o se deve utilizzare una delle opzioni più lente.

In Python, un compilatore potrebbe teoricamente fare quasi che buono se sapesse che ae berano entrambi ints. C'è un ulteriore sovraccarico di boxe, ma se i tipi fossero staticamente noti potresti probabilmente sbarazzartene anche tu (pur presentando l'interfaccia che gli interi sono oggetti con metodi, gerarchia di superclassi, ecc.). Il problema è che un compilatore per Python non puòsappi questo, poiché le classi sono definite in fase di esecuzione, possono essere modificate in fase di esecuzione e anche i moduli che eseguono la definizione e l'importazione vengono risolti in fase di esecuzione (e anche quali istruzioni di importazione vengono eseguite dipende da cose che possono essere conosciute solo in fase di esecuzione). Quindi il compilatore Python dovrebbe sapere quale codice è stato eseguito (ovvero risolvere il problema di Halting) per sapere quale sarà la dichiarazione che sta compilando.

Quindi, anche con le analisi più sofisticate che sono teoricamente possibili , semplicemente non si può dire molto su ciò che una determinata istruzione Python farà in anticipo. Ciò significa che anche se un sofisticato compilatore Python fosse implementato, in quasi tutti i casi dovrebbe comunque emettere codice macchina che segue il protocollo di ricerca del dizionario Python per determinare la classe di un oggetto e trovare metodi (attraversando l'MRO della gerarchia di classi, che può anche cambiare dinamicamente in fase di esecuzione e quindi è difficile da compilare in una semplice tabella di metodi virtuali), e sostanzialmente fare ciò che fanno gli interpreti (lenti). Questo è il motivo per cui non esistono davvero compilatori di ottimizzazione sofisticati per linguaggi dinamici. Non è semplicemente difficile crearne uno, il massimo profitto possibile non è

Nota che questo non è basato su ciò che il codice sta facendo, è basato su ciò che il codice potrebbe fare. Anche il codice Python che è una semplice serie di operazioni aritmetiche intere deve essere compilato come se potesse invocare operazioni di classe arbitrarie. I linguaggi statici hanno maggiori restrizioni sulle possibilità di ciò che il codice potrebbe fare, e di conseguenza i loro compilatori possono fare più ipotesi.

I compilatori JIT ottengono questo vantaggio aspettando il runtime per compilare / ottimizzare. Ciò consente loro di emettere codice che funziona per ciò che il codice sta facendo piuttosto che per quello che potrebbe fare. E a causa di ciò i compilatori JIT hanno un potenziale di pagamento molto più grande per i linguaggi dinamici che per i linguaggi statici; per linguaggi più statici molto di ciò che un ottimizzatore vorrebbe sapere può essere conosciuto in anticipo, quindi potresti anche ottimizzarlo, lasciando meno a un compilatore JIT.

Esistono vari compilatori JIT per linguaggi dinamici che affermano di raggiungere velocità di esecuzione paragonabili a quelle del C / C ++ compilato e ottimizzato. Ci sono anche ottimizzazioni che possono essere fatte da un compilatore JIT che non possono essere eseguite da un compilatore in anticipo per qualsiasi lingua, quindi teoricamente la compilazione JIT (per alcuni programmi) potrebbe un giorno superare il miglior compilatore statico possibile. Ma, come giustamente sottolineato da Devin, le proprietà della compilazione JIT (solo gli "hotspot" sono veloci e solo dopo un periodo di riscaldamento) indicano che è improbabile che i linguaggi dinamici compilati JIT siano mai adatti a tutte le possibili applicazioni, anche se diventano più veloce o più veloce delle lingue compilate staticamente in generale.


1
Questo è ora due voti negativi senza commenti. Gradirei suggerimenti su come migliorare questa risposta!
Ben

Non ho espresso il mio voto negativo, ma non sei corretto riguardo alla "necessità di risolvere il problema dell'arresto". È stato dimostrato in molte circostanze che il codice in linguaggi dinamici può essere compilato in modo ottimale, mentre per quanto ne
so

@mikera Mi dispiace, ma no, non sei corretto. Nessuno ha mai implementato un compilatore (nel senso che comprendiamo che GCC è un compilatore) per Python completamente generale o altri linguaggi dinamici. Ogni sistema di questo tipo funziona solo per un sottoinsieme della lingua, o solo per determinati programmi, o talvolta emette codice che è fondamentalmente un interprete contenente un programma hardcoded. Se lo desideri, ti scriverò un programma Python contenente la riga in foo = x + ycui la previsione del comportamento dell'operatore addizione al momento della compilazione dipende dalla risoluzione del problema di arresto.
Ben

Ho ragione e penso che manchi il punto. Ho detto "in molte circostanze". Non ho detto "in tutte le circostanze possibili". Il fatto che tu possa o meno costruire un esempio inventato legato al problema dell'arresto è in gran parte irrilevante nel mondo reale. FWIW, potresti anche costruire un esempio simile per C ++ in modo da non provare comunque nulla. Comunque, non sono venuto qui per discutere, solo per suggerire miglioramenti alla tua risposta. Prendere o lasciare.
Mikera,

@mikea Penso che potresti perdere il punto. Per compilare x + yoperazioni efficienti di aggiunta della macchina, è necessario sapere in fase di compilazione se lo è o meno. Per tutto il tempo , non solo in parte. Per i linguaggi dinamici questo non è quasi mai possibile con programmi realistici, anche se l'euristica ragionevole indovinerebbe il più delle volte. La compilazione richiede garanzie in fase di compilazione . Quindi parlando di "in molte circostanze" in realtà non stai rispondendo alla mia risposta.
Ben

9

Solo un puntatore rapido che delinea lo scenario peggiore per i linguaggi dinamici:

L'analisi del Perl non è calcolabile

Di conseguenza, il Perl (completo) non può mai essere compilato staticamente.


In generale, come sempre, dipende. Sono fiducioso che se si tenta di emulare funzionalità dinamiche in un linguaggio compilato staticamente, interpreti ben concepiti o varianti (parzialmente) compilate possono avvicinarsi o ridurre le prestazioni dei linguaggi compilati staticamente.

Un altro punto da tenere a mente è che i linguaggi dinamici risolvono un altro problema rispetto a C. C è poco più che una bella sintassi per l'assemblatore, mentre i linguaggi dinamici offrono ricche astrazioni. Le prestazioni di runtime spesso non sono la preoccupazione principale: il time-to-market, ad esempio, dipende dal fatto che i tuoi sviluppatori siano in grado di scrivere sistemi complessi e di alta qualità in tempi brevi. L'estensibilità senza ricompilazione, ad esempio con i plugin, è un'altra caratteristica popolare. Quale lingua preferisci in questi casi?


5

Nel tentativo di offrire una risposta più obiettivamente scientifica a questa domanda, sostengo quanto segue. Un linguaggio dinamico richiede un interprete, o runtime, per prendere decisioni in fase di esecuzione. Questo interprete, o runtime, è un programma per computer e, come tale, è stato scritto in un linguaggio di programmazione, statico o dinamico.

Se l'interprete / runtime è stato scritto in un linguaggio statico, allora si potrebbe scrivere un programma in quel linguaggio statico che (a) svolge la stessa funzione del programma dinamico che interpreta e (b) esegue anche almeno. Speriamo che questo sia evidente, poiché fornire una prova rigorosa di queste affermazioni richiederebbe uno sforzo aggiuntivo (forse considerevole).

Supponendo che queste affermazioni siano vere, l'unica via d'uscita è richiedere che anche l'interprete / il runtime siano scritti in un linguaggio dinamico. Tuttavia, incontriamo lo stesso problema di prima: se l'interprete è dinamico, richiede un interprete / runtime, che deve anche essere stato scritto in un linguaggio di programmazione, dinamico o statico.

A meno che tu non presuma che un'istanza di un interprete sia in grado di interpretare se stessa in fase di esecuzione (spero che ciò sia evidentemente assurdo), l'unico modo per battere i linguaggi statici è che ogni istanza di un interprete sia interpretata da un'istanza di interprete separata; questo porta a un regresso infinito (spero che questo sia evidentemente assurdo) o a un ciclo chiuso di interpreti (spero che sia anche evidentemente assurdo).

Sembra quindi che, anche in teoria, i linguaggi dinamici non possano funzionare meglio dei linguaggi statici, in generale. Quando si usano modelli di computer realistici, sembra ancora più plausibile; dopotutto, una macchina può eseguire solo sequenze di istruzioni macchina e tutte le sequenze di istruzioni macchina possono essere compilate staticamente.

In pratica, la corrispondenza delle prestazioni di un linguaggio dinamico con un linguaggio statico potrebbe richiedere la reimplementazione dell'interprete / runtime in un linguaggio statico; tuttavia, che puoi farlo è il punto cruciale di questo argomento. È una domanda su pollo e uova e, purché tu sia d'accordo con i presupposti non dimostrati (anche se, a mio avviso, per lo più evidenti) fatti sopra, possiamo effettivamente rispondere; dobbiamo dare un cenno al linguaggio statico, non dinamico.

Un altro modo di rispondere alla domanda, alla luce di questa discussione, è questo: nel programma memorizzato, controllo = modello di dati dell'informatica che sta al cuore dell'informatica moderna, la distinzione tra compilazione statica e dinamica è una falsa dicotomia; i linguaggi compilati staticamente devono avere un mezzo per generare ed eseguire codice arbitrario in fase di esecuzione. È fondamentalmente correlato al calcolo universale.


Rileggendo questo, non credo sia vero. La compilazione JIT rompe il tuo argomento. Anche il codice più semplice, ad esempio, main(args) { for ( i=0; i<1000000; i++ ) { if ( args[0] == "1" ) {...} else {...} }può accelerare in modo significativo una volta che il valore di argsè noto (supponendo che non cambi mai, cosa che potremmo essere in grado di affermare). Un compilatore statico non può creare codice che interrompa mai il confronto. (Certo, in quell'esempio basta tirare iffuori il giro. Ma la cosa potrebbe essere più contorta.)
Raffaello

@Raphael Penso che JIT probabilmente metta in discussione. I programmi che eseguono la compilazione JIT (ad esempio, JVM) sono in genere programmi compilati staticamente. Se un programma JIT compilato staticamente potrebbe eseguire uno script più velocemente di un altro programma statico, potrebbe fare lo stesso lavoro, basta "raggruppare" lo script con il compilatore JIT e chiamare il pacchetto un programma compilato staticamente. Questo deve fare almeno quanto la JIT che opera su un programma dinamico separato, contraddicendo qualsiasi argomento secondo il quale la JIT deve fare di meglio.
Patrick87,

Hm, è come dire che raggruppare uno script Ruby con il suo interprete ti dà un programma compilato staticamente. Non sono d'accordo (poiché rimuove tutte le distinzioni delle lingue in questo senso), ma questo è un problema di semantica, non di concetti. Concettualmente, adattare il programma in fase di esecuzione (JIT) può fare ottimizzazioni che non si possono fare in fase di compilazione (compilatore statico), e questo è il mio punto.
Raffaello

@Raphael Il fatto che non ci sia una distinzione significativa è una sorta di punto della risposta: qualsiasi tentativo di classificare rigidamente alcune lingue come statiche, e quindi affette da limiti di prestazioni, fallisce esattamente per questo motivo: non c'è una differenza convincente tra un preconfezionato (Ruby , script) e un programma C. Se (Ruby, script) può fare in modo che la macchina esegua la giusta sequenza di istruzioni per risolvere efficacemente un determinato problema, lo stesso potrebbe fare un programma C abilmente realizzato.
Patrick87,

Ma puoi definire la differenza. Una variante invia il codice a portata di mano al processore invariato (C), l'altra viene compilata in fase di esecuzione (Ruby, Java, ...). Il primo è ciò che intendiamo per "compilazione statica" mentre il secondo sarebbe "compilazione just in time" (che consente ottimizzazioni dipendenti dai dati).
Raffaello

4

Riesci a creare compilatori per linguaggi dinamici come Ruby per avere prestazioni simili e comparabili a C / C ++?

Penso che la risposta sia "sì" . Credo anche che possano persino superare l'attuale architettura C / C ++ in termini di efficienza (anche se leggermente).

Il motivo è semplice: ci sono più informazioni in fase di esecuzione che in fase di compilazione.

I tipi dinamici sono solo un piccolo ostacolo: se una funzione viene sempre o quasi sempre eseguita con gli stessi tipi di argomenti, un ottimizzatore JIT può generare un ramo e un codice macchina per quel caso specifico. E c'è molto di più che si può fare.

Vedi Dynamic Languages ​​Strike Back , un discorso di Steve Yegge di Google (c'è anche una versione video da qualche parte credo). Cita alcune tecniche di ottimizzazione JIT concrete da V8. Inspiring!

Non vedo l'ora di cosa avremo nei prossimi 5 anni!


2
Adoro l'ottimismo.
Dave Clarke,

Credo che ci siano state alcune critiche molto specifiche sulle imprecisioni nel discorso di Steve. Li posterò quando li troverò.
Konrad Rudolph,

1
@DaveClarke è quello che mi fa correre :)
Kos,

2

Le persone che apparentemente pensano che ciò sia teoricamente possibile, o in un futuro lontano, sono completamente sbagliate secondo me. Il punto sta nel fatto che i linguaggi dinamici forniscono e impongono uno stile di programmazione totalmente diverso. In realtà, la differenza è duplice, anche se entrambi gli aspetti sono correlati:

  • I simboli (var, o piuttosto collegamenti <>) datum di ogni tipo) non sono tipizzati.
  • Le strutture (i dati, tutto ciò che vive in fase di esecuzione) non sono tipizzate dai tipi dei loro elementi.

Il secondo punto fornisce generalità gratuitamente. Si noti che le strutture qui sono elementi compositi, raccolte, ma anche tipi stessi e persino (!) Routine di tutti i tipi (funzioni, azioni, operazioni) ... Potremmo digitare le strutture in base ai loro tipi di elementi, ma a causa del primo punto il il controllo avverrebbe comunque durante l'esecuzione. Potremmo avere simboli digitati e comunque avere quelli strutturati non tipizzati in base al tipo di elemento (un array averrebbe semplicemente digitato come un array non come un array di ints), ma anche questo pochi non è vero in un linguaggio dinamico ( apotrebbe anche contenere una stringa).

L

  • ElementLL
  • ElementL
  • tutte le strutture (di nuovo, comprese le routine dei modelli) ricevono solo gli Element

È chiaro per me che questa è solo un'enorme penalità per perf; e non tocco nemmeno tutte le conseguenze (la miriade di controlli di runtime di tutti i tipi necessari per garantire la sensibilità del programma) ben descritte in altri post.


+1 Molto interessante. Hai letto la mia risposta? Il tuo e il mio pensiero sembrano simili, sebbene la tua risposta fornisca maggiori dettagli e una prospettiva interessante.
Patrick87,

Le lingue dinamiche non devono essere non tipizzate. L'implementazione di un modello di un linguaggio dinamico in C limita fortemente le possibilità di ottimizzazione; rende molto difficile per il compilatore riconoscere ottimizzazioni di alto livello (ad es. dati immutabili) e costringe alcune operazioni fondamentali (ad es. chiamate di funzione) a passare attraverso C. Ciò che descrivi non è lontano da un interprete di bytecode, con la decodifica di bytecode preevaluated; i compilatori nativi tendono ad avere prestazioni significativamente migliori, dubito che la decodifica del bytecode possa giustificare la differenza.
Gilles 'SO- smetti di essere malvagio' il

@ Patrick87: hai ragione, le nostre linee di pensiero sembrano molto simili (non avevo mai letto prima, scusa, la mia riflessione deriva dall'attuazione di un lang din in C).
spir

@Gilles: Sono piuttosto d'accordo sul fatto che "... non debba essere tipizzato", se intendi non digitato staticamente . Ma questo non è ciò che la gente pensa delle dinastie in generale, immagino. Personalmente considero la genericità (nel senso generale data nella risposta sopra) una caratteristica molto più potente e molto più difficile da vivere senza. Possiamo facilmente trovare modi di fare con i tipi statici allargando il nostro pensiero su come può essere definito un dato tipo (apparentemente polimorfico) o dando più flessibilità direttamente alle istanze.
spir

1

Non ho avuto il tempo di leggere tutte le risposte in dettaglio ... ma ero divertito.

Ci fu una controversia simile negli anni sessanta e all'inizio degli anni settanta (la storia dell'informatica si ripete spesso): si possono compilare linguaggi di alto livello per produrre codice efficiente come il codice macchina, diciamo il codice assembly, prodotto manualmente da un programmatore. Tutti sanno che un programmatore è molto più intelligente di qualsiasi programma e può trovare un'ottimizzazione molto intelligente (pensando in realtà principalmente a ciò che ora viene chiamato ottimizzazione spioncino). Questa è ovviamente ironia da parte mia.

C'era anche un concetto di espansione del codice: il rapporto tra la dimensione del codice prodotto da un compilatore e la dimensione del codice per lo stesso programma prodotto da un buon programmatore (come se ce ne fossero stati troppi :-). Naturalmente l'idea era che questo rapporto fosse sempre maggiore di 1. Le lingue dell'epoca erano Cobol e Fortran 4, o Algol 60 per gli intellettuali. Credo che Lisp non sia stato preso in considerazione.

Bene, c'erano alcune voci secondo cui qualcuno aveva prodotto un compilatore che a volte poteva ottenere un rapporto di espansione di 1 ... fino a quando non è diventata semplicemente la regola che il codice compilato era molto meglio del codice scritto a mano (e anche più affidabile). Le persone erano preoccupate per la dimensione del codice in quei tempi (piccoli ricordi), ma lo stesso vale per la velocità o il consumo di energia. Non entrerò nei motivi.

Funzionalità strane, funzionalità dinamiche di una lingua non contano. Ciò che conta è come vengono utilizzati, sia che vengano utilizzati. Le prestazioni, in qualunque unità (dimensioni del codice, velocità, energia, ...) dipendono spesso da parti di programmi molto piccole. Quindi c'è una buona possibilità che le strutture che danno potere espressivo non si mettano davvero in mezzo. Con una buona pratica di programmazione, le strutture avanzate vengono utilizzate solo in modo disciplinato, per immaginare nuove strutture (questa è stata la lezione lisp).

Il fatto che una lingua non abbia una tipizzazione statica non ha mai significato che i programmi scritti in quella lingua non siano tipizzati staticamente. D'altra parte, potrebbe darsi che il sistema di tipi utilizzato da un programma non sia ancora sufficientemente formalizzato perché esista un verificatore di tipi.

Ci sono stati, nella discussione, diversi riferimenti all'analisi del caso peggiore ("arresto del problema", analisi del PERL). Ma l'analisi del caso peggiore è per lo più irrilevante. Ciò che conta è ciò che accade nella maggior parte dei casi o in casi utili ... comunque definiti o compresi o vissuti. Ecco un'altra storia, direttamente correlata all'ottimizzazione del programma. Si è svolto molto tempo fa in una grande università del Texas, tra uno studente di dottorato e il suo consulente (che è stato successivamente eletto in una delle accademie nazionali). Per quanto ricordo, lo studente ha insistito per studiare un problema di analisi / ottimizzazione che il consulente aveva dimostrato essere irrintracciabile. Presto non furono più in termini di parola. Ma lo studente aveva ragione: il problema era abbastanza trattabile nella maggior parte dei casi pratici in modo che la tesi da lui prodotta diventasse un lavoro di riferimento.

E per commentare ulteriormente l'affermazione che Perl parsing is not computable, qualunque cosa si intenda con quella frase, c'è un problema simile con ML, che è un linguaggio straordinariamente ben formalizzato. Type checking complexity in ML is a double exponential in the lenght of the program.Questo è un risultato molto preciso e formale nella complessità del caso peggiore ... che non ha alcuna importanza. Dopo tutto, gli utenti ML stanno ancora aspettando un programma pratico che esploderà il controllo del tipo.

In molti casi, com'era prima, il tempo e la competenza dell'uomo sono più scarsi della potenza di calcolo.

Il vero problema del futuro sarà far evolvere i nostri linguaggi per integrare nuove conoscenze, nuove forme di programmazione, senza dover riscrivere tutto il software legacy ancora in uso.

Se guardi la matematica, è una conoscenza molto ampia. Le lingue usate per esprimerlo, le notazioni e i concetti si sono evoluti nel corso dei secoli. È facile scrivere vecchi teoremi con i nuovi concetti. Adattiamo le prove principali, ma non ci preoccupiamo per molti risultati.

Ma nel caso della programmazione, potremmo dover riscrivere tutte le prove da zero (i programmi sono prove). Può darsi che ciò di cui abbiamo veramente bisogno siano linguaggi di programmazione evolutivi e di altissimo livello. I progettisti di Optimizer saranno felici di seguire.


0

Un paio di note:

  • Non tutte le lingue di alto livello sono dinamiche. Haskell è di altissimo livello, ma è completamente tipizzato staticamente. Anche i sistemi che programmano linguaggi come Rust, Nim e D possono esprimere astrazioni di alto livello in modo succinto ed efficiente. In realtà, possono essere concisi quanto i linguaggi dinamici.

  • Esistono compilatori anticipatamente altamente ottimizzati per linguaggi dinamici. Le buone implementazioni Lisp raggiungono la metà della velocità dell'equivalente C.

  • La compilation JIT può essere una grande vittoria qui. Il Web Application Firewall di CloudFlare genera il codice Lua che viene eseguito da LuaJIT. LuaJIT ottimizza fortemente i percorsi di esecuzione effettivamente intrapresi (in genere, i percorsi di non attacco), con il risultato che il codice viene eseguito molto più velocemente del codice prodotto da un compilatore statico sul carico di lavoro effettivo. A differenza di un compilatore statico con ottimizzazione guidata dal profilo, LuaJIT si adatta alle modifiche nei percorsi di esecuzione in fase di esecuzione.

  • Anche la de-ottimizzazione è cruciale. Invece del codice compilato da JIT che deve verificare la presenza di una classe di cui viene eseguito il monkeypatch, l'atto del monkeypatch attiva un hook nel sistema di runtime che scarta il codice del computer che dipendeva dalla vecchia definizione.


Come è una risposta? Bene, proiettile tre forse, se hai aggiunto riferimenti.
Raffaello

Sono molto scettico sull'affermazione secondo cui PGO non poteva eguagliare le prestazioni di LuaJIT per il codice dell'applicazione Web con un carico di lavoro tipico.
Konrad Rudolph,

@KonradRudolph Il vantaggio principale di una JIT è che la JIT adatta il codice quando diversi percorsi diventano caldi.
Demi

@Demetri Lo so. Ma è molto difficile quantificare se questo sia un vantaggio - vedi la mia risposta e la discussione dei commenti lì. In breve: mentre JIT può adattarsi ai cambiamenti nell'utilizzo, deve anche tenere traccia delle cose in fase di esecuzione, il che comporta un sovraccarico. Il pareggio per questo è intuitivamente solo dove si verificano frequenti cambiamenti nel comportamento. Per le app Web, probabilmente esiste solo un singolo (o pochissimo) modello di utilizzo per il quale l'ottimizzazione paga, quindi l'aumento minimo delle prestazioni dovuto all'adattabilità non compensa le spese generali della profilazione continua.
Konrad Rudolph,
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.