L'eredità prototipale è intrinsecamente più lenta?

7

Vedo che Javascript 6 aggiungerà ereditarietà basata sulla classe tradizionale e un argomento che sento è che le classi sono intrinsecamente molto più veloci dei prototipi perché possono essere ottimizzate dal compilatore quando si esegue la compilazione (JIT) al codice nativo o al bytecode. Quel genere di cose ha senso per me, ma non sono un programmatore abbastanza avanzato per capire davvero questa roba.

L'argomento che ho sempre visto per l'eredità prototipale (PI) è che conserva la memoria, che in questi giorni è per lo più un punto controverso. Personalmente, mi piace PI e ho sempre sentito che "ha senso" in un linguaggio dinamico; come se fosse un lusso che puoi anche permettertelo se hai già optato per i limiti prestazionali di un linguaggio dinamico interpretato.

In C ++, le classi hanno un senso a causa delle limitazioni che derivano dalla filosofia C ++ che tutto passa in secondo piano alla massima possibilità di rendimento.

Ovviamente puoi avere PI in C ++ e c'è un modello di design ben noto, ma sto parlando dell'ereditarietà integrata in un linguaggio dinamico.

Se si guardava all'implementazione più veloce possibile dell'ereditarietà prototipale in un linguaggio dinamico fittizio, quali sarebbero necessariamente gli inconvenienti in termini di prestazioni rispetto alle classi tradizionali?

Che cosa succede se aggiungi la funzione al linguaggio fittizio che puoi specificare che alcuni oggetti sono "definitivi" e che la loro catena di prototipi non può essere modificata in fase di runtime? (Coesistente con PI tradizionale) Aggiunge anche la digitazione statica facoltativa o il tipo di suggerimento se questo aiuta il linguaggio prototipo.

Poiché questa potrebbe essere considerata una domanda un po 'vaga, aggiungo:

Credo che cerco le riflessioni sull'argomento da persone più intelligenti e più interessate alla teoria del linguaggio e / o alla scrittura di compilatori / interpreti di me. Spero che sia ancora una domanda ok.

Non si tratta affatto di Javascript 6, l'ho appena presentato perché Javascript è il linguaggio prototipo di maggior successo ed è interessante che aggiungano classi native anche se Javascript sembra andasse bene senza di loro (sia la popolarità sia negli ultimi anni ha avuto alcuni sbalorditivi miglioramenti nei benchmark). Inoltre, non sto dicendo che credo o penso che Javascript sia lento.

In realtà, sono più interessato a saperne di più sulla programmazione dei linguaggi, possibilmente con l'obiettivo di costruire un giorno il mio linguaggio dinamico.

    
posta Jonathan J. Bloggs 05.02.2015 - 03:44
fonte

3 risposte

4

Ci sono davvero due cose da capire:

  1. l'ereditarietà prototipale non ha nulla a che fare con le prestazioni a tutti . I problemi di rendimento derivano dalle modifiche runtime alla struttura di ereditarietà.
  2. l'ereditarietà prototipale (e le strutture di oggetti flessibili) non sono tanto più intrinsecamente lente, quanto più difficili da ottimizzare.

Per illustrare la prima affermazione, Ruby sarebbe un esempio di punta di oggetti basati su classi che sono abissalmente lenti. Il problema persiste generalmente durante tutte le lingue simili a Smalltalk, ad es. Objective-C, che impiega il messaggio che passa per le chiamate di metodo. Il runtime Objective-C utilizza un po 'di caching del metodo elegante per affrontare il problema, ma ci sono voluti un po' per arrivare così lontano.

Ciò che rende convenienti le chiamate al metodo è che il compilatore (o JIT o anche il runtime) ha una certa conoscenza sulla struttura di un oggetto. Sia che sia dato esplicitamente in termini di caratteristiche del linguaggio, o implicitamente dedotto dall'analisi statica, la conoscenza esiste e il compilatore può utilizzarlo per ottimizzare.

Ora, quando la struttura può cambiare in fase di esecuzione, ciò rende le cose un po 'complicate, perché hai bisogno di una buona euristica per rilevare quali porzioni del codice meritano di essere ottimizzate (vuoi un buon rapporto tra la frequenza del codice correre alla frequenza con cui cambia e al costo di ottimizzarlo), per ottenere le migliori caratteristiche generali di runtime.

Quindi qual è il punto delle lezioni nelle lingue dinamiche? Beh, ce ne deve essere uno, perché esistono numerosi sistemi di classi JavaScript (come quello di Ext). Certo, la familiarità per i programmatori abituati alle classi è una delle ragioni. Ma il vero vantaggio deriva dall'aiutare a garantire definizioni esplicite dei tipi di oggetto. Tale definizione di classe è una (anche se complessa). Con i costruttori JavaScript vanilla, hai un sacco di dichiarazioni, che sono raggruppate insieme, se sei fortunato. La struttura di classe è in realtà solo un effetto collaterale del codice imperativo, se vuoi. Le dichiarazioni di classe non sono sorprendentemente destinate a facilitare uno stile più dichiarativo.

Vale la pena notare che il linguaggio Self che ha reso esplicita l'eredità prototipale (sebbene sia davvero semplice da ottenere in una lingua con il passaggio dei messaggi), è stato creato per un ambiente in cui hai programmato un sistema completamente interattivo mentre era in esecuzione. Si potrebbe effettivamente vedere un oggetto sullo schermo. Ciò consentiva una dichiarazione pulita che era ancora modificabile in fase di esecuzione, perché la dichiarazione e il risultato erano così intimamente accoppiati. Senza un tale accoppiamento, giocare con la struttura degli oggetti in fase di esecuzione può rapidamente trasformarsi in un pasticcio incomprensibile che è davvero difficile da inserire nel tuo cervello, per non parlare della ragione.

È possibile ottenere praticamente un'eredità prototipale se si dispone di un buon supporto per la delega. È sufficiente delegare tutte le chiamate non implementate a un oggetto che consideri un prototipo e il gioco è fatto. È più flessibile Tuttavia è ugualmente più difficile da ottimizzare.

    
risposta data 05.02.2015 - 13:00
fonte
1

In C++, classes makes sense because of the limitations that comes with the C++ philosophy that everything takes a back seat to maximum possibility for performance.

Hai due false premesse inerenti a questa affermazione. Il primo è che le classi sono una cosa C ++. Non lo sono; il concetto è antico quanto lo stesso OOP, risalente a Simula, il primo linguaggio orientato agli oggetti, e quasi ogni linguaggio OO da allora ha avuto classi, incluse quelle dinamiche come Python. È il modello prototipo che è l'aberrazione.

Il secondo è che le classi vengono utilizzate a causa delle prestazioni. Anche se questo è sicuramente un vantaggio, è difficile che il sia il motivo per utilizzarli. I veri motivi per usare le classi sono che sono un modo naturale per modellare molti tipi importanti di problemi di programmazione e che si adattano molto bene alla tipizzazione statica come modo per rafforzare la correttezza e affrontare intere classi di problemi (nessun gioco di parole ) in fase di compilazione.

E sì, le classi sono intrinsecamente più veloci in fase di esecuzione rispetto all'ereditarietà prototipale, per un semplice motivo: qualsiasi informazione che può essere calcolata in fase di compilazione non deve essere calcolata in fase di runtime. Le classi forniscono il compilatore con alcuni invarianti che può sapere in anticipo non verrà violato, il che gli consente di creare un codice runtime più semplice che non ha bisogno di verificare tali elementi.

A seconda di come la classe è implementata, può esserci un secondo vantaggio importante: se gli oggetti sono implementati come record contigui con campi, l'accesso ai membri diventa una dereferenziazione di un puntatore a un offset costante dall'inizio della memoria dell'oggetto posizione, che sarà dozzine o anche centinaia di volte più veloce di cercare i membri in una tabella hash o in un'altra mappa, che è come devi farlo in fase di esecuzione se il layout dell'oggetto non è noto al compilatore.

Quindi sì, le classi sono intrinsecamente più veloci, motivo per cui le JIT JavaScript moderne cercano di trasformare gli oggetti in classi quando possibile. Ma questo è solo un vantaggio collaterale; il vero vantaggio deriva dal fatto che sono intrinsecamente più sicuri e più utili come strumento per ragionare e provare le cose sul comportamento del tuo codice.

    
risposta data 05.02.2015 - 04:40
fonte
0

AFAIK, non c'è molta differenza tra i due. Considera che una classe C ++ è in realtà un blocco di dati con una speciale struttura di dati nascosta all'inizio denominata vtable. Questo vtable contiene tutti i puntatori ai metodi contenuti all'interno della classe.

Ora vedi che non è molto diverso da un prototipo di javascript - che è un mucchio di indicatori di funzioni che comprendono la classe js.

Tuttavia, in pratica la differenza è enorme. Il C ++ ti limita a non essere in grado di aggiungere, rimuovere o sostituire dinamicamente quei puntatori di funzione. Ciò significa che il compilatore sa in anticipo cosa sono e quindi può ottimizzarli completamente. Significa anche che sa esattamente dove si trovano le funzioni e può anche integrare i metodi se ritiene che ne valga la pena (per molti metodi di proprietà, getID () {return id;}, ad esempio, questo ha perfettamente senso).

Quindi in un binario C ++, ottieni spesso il codice che è semplicemente una procedura lineare delle istruzioni del programma. La CPU caricherà blocchi di codice nella cache ed eseguirli il più velocemente possibile. Questo è ciò che rende le moderne CPU.

Dove le CPU smettono di funzionare è quando devono fermarsi e caricare un blocco di codice da qualche altra parte. Quindi, nel caso di un programma javascript in cui le funzioni non sono conosciute al momento della compilazione e potrebbero essere ovunque, il codice del programma compilato invece di saltare le istruzioni al percorso del metodo. Se sei sfortunato, questo può significare che la CPU deve interrompere ciò che sta facendo, caricare un nuovo blocco di codice contenente il metodo, eseguirlo e il ritorno da dove proviene. Puoi vedere che questo è molto meno efficiente della semplice lettura di "id" nel mio semplice esempio.

Si noti che questa non è una cosa C ++. I primi compilatori C + sono stati scritti come traduttori in C, quindi una classe C ++ sarebbe stata trasformata in codice C che aveva una struct e un set di puntatori a funzione che sarebbero poi stati compilati da un compilatore C.

Ho sorvolato molte complicazioni (incluso un po 'di polimorfismo C ++) e ottimizzazioni e' workaround 'CPU e compilatori possono fare per far funzionare meglio questo, ma si ottiene l'idea fondamentale, principalmente che l'elaborazione del tempo di compilazione è più veloce di elaborazione runtime.

Se vuoi progettare un linguaggio veloce, rendilo così tante informazioni possono essere calcolate nella fase di compilazione in modo che non debba essere fatto in fase di runtime. È possibile progettare un sistema di classi basato su prototipi e, se è possibile analizzare l'intero programma e capire in anticipo dove si trovava ogni funzione, è possibile compilarlo su un codice macchina eseguito come su un programma C ++. Il problema è che un simile parsing potrebbe essere più complesso che eseguirlo! (nota che alcuni compilatori C ++ hanno un meccanismo di ottimizzazione del tempo di esecuzione in cui vengono calcolate le informazioni di runtime e utilizzate per tornare alle successive compilazioni!)

    
risposta data 05.02.2015 - 11:32
fonte