Quali sono le sfide legate alla digitazione nello scrivere un compilatore per un linguaggio tipizzato in modo dinamico?


9

In questo discorso , Guido van Rossum sta parlando (27:30) dei tentativi di scrivere un compilatore per il codice Python, commentandolo dicendo:

risulta che non è così facile scrivere un compilatore che mantenga tutte le belle proprietà di digitazione dinamica e mantenga anche la correttezza semantica del tuo programma, in modo che faccia effettivamente la stessa cosa, indipendentemente dal tipo di stranezza che fai da qualche parte sotto le copertine e effettivamente funziona più veloce

Quali sono le (possibili) sfide legate alla scrittura di un compilatore per un linguaggio tipizzato in modo dinamico come Python?


In questo caso la digitazione dinamica non è quasi il problema più grande. Per Python è un ambito dinamico.
SK-logic,

Vale la pena notare che altre persone hanno sostenuto che la digitazione dinamica nella piattaforma è la risposta giusta qui. Microsoft ha investito molti soldi nel DLR proprio per questo motivo - e NeXT / Apple è stata a metà strada su quel carrozzone per decenni. Ciò non aiuta CPython, ma IronPython dimostra che è possibile compilare staticamente Python in modo efficace e PyPy dimostra che non è necessario.
Abarnert,

2
@ Scoping dinamico della logica SK in Python? L'ultima volta che ho controllato, tutti i costrutti nella lingua usano l'ambito lessicale.

1
@ SK-logic È possibile creare in modo dinamico il codice ed eseguirlo, ma tale codice viene eseguito anche in ambito lessicale. Per ogni singola variabile in un programma Python, puoi facilmente determinare a quale ambito appartiene una variabile semplicemente controllando l'AST. Potresti pensare execall'affermazione , che è scomparsa dal 3.0 e quindi al di fuori della mia considerazione (e probabilmente di Guido, come il discorso è del 2012). Potresti fare un esempio? E la tua definizione di "scoping dinamico", se è [diverso dal mio] (en.wikipedia.org/wiki/Dynamic_scoping).

1
@ SK-logic L'unica cosa che è un dettaglio di implementazione per me sono le modifiche per restituire il valore del locals()persistere tra le chiamate locals. Ciò che è documentato e sicuramente non un dettaglio di implementazione è che nemmeno localso globalspuò cambiare in quale ambito viene cercata ogni variabile. Per ogni singolo uso di una variabile, l'ambito a cui si fa riferimento è determinato staticamente. Il che lo rende decisamente con finalità lessicale. (E a proposito, evale execsicuramente non sono nemmeno i dettagli di implementazione - guarda la mia risposta!)

Risposte:


16

Hai semplificato troppo l'affermazione di Guido nel formulare la tua domanda. Il problema non è scrivere un compilatore per un linguaggio tipizzato in modo dinamico. Il problema è quello di scriverne uno che sia (criterio 1) sempre corretto, (criterio 2) mantiene la digitazione dinamica e (criterio 3) è notevolmente più veloce per una quantità significativa di codice.

È facile implementare il 90% (in mancanza dei criteri 1) di Python ed essere costantemente veloce. Allo stesso modo, è facile creare una variante Python più veloce con la digitazione statica (criterio 2 non riuscito). L'implementazione del 100% è anche facile (nella misura in cui è facile implementare un linguaggio così complesso), ma finora ogni modo semplice di implementarlo risulta relativamente lento (in mancanza dei criteri 3).

L'implementazione di un interprete più JIT che è corretta, implementa l'intero linguaggio ed è più veloce per alcuni codici risulta fattibile, sebbene significativamente più difficile (cfr. PyPy) e solo così se automatizzi la creazione del compilatore JIT (Psyco ha fatto a meno , ma era molto limitato in quale codice poteva accelerare). Ma nota che questo è esplicitamente fuori portata, poiché stiamo parlando di elettricità staticacompilatori (noti anche in anticipo). Cito solo questo per spiegare perché il suo approccio non funziona per i compilatori statici (o almeno non esiste un controesempio esistente): deve prima interpretare e osservare il programma, quindi generare codice per una iterazione specifica di un ciclo (o un altro codice lineare percorso), quindi ottimizza l'inferno in base a ipotesi vere solo per quella specifica iterazione (o almeno, non per tutte le possibili iterazioni). L'aspettativa è che molte esecuzioni successive di quel codice corrispondano anche alle aspettative e quindi traggono vantaggio dalle ottimizzazioni. Alcuni controlli (relativamente economici) vengono aggiunti per garantire la correttezza. Per fare tutto ciò, è necessario avere un'idea di cosa specializzarsi e un'implementazione lenta ma generale a cui ricorrere. I compilatori AOT non hanno nessuno dei due. Non possono specializzarsi affattoin base al codice che non possono vedere (ad es. codice caricato dinamicamente) e specializzarsi con noncuranza significa generare più codice, il che presenta una serie di problemi (utilizzo di icache, dimensione binaria, tempo di compilazione, rami aggiuntivi).

L'implementazione di un compilatore AOT che implementa correttamente l' intera lingua è anche relativamente semplice: generare codice che chiama nel runtime per fare ciò che l'interprete farebbe se alimentato con questo codice. Nuitka (principalmente) lo fa. Tuttavia, ciò non produce molti vantaggi in termini di prestazioni (in mancanza dei criteri 3), poiché devi ancora fare tutto il lavoro non necessario di un interprete, salvo per inviare il bytecode al blocco di codice C che fa quello che hai compilato. questo è solo un costo piuttosto piccolo - abbastanza significativo da valere la pena di essere ottimizzato in un interprete esistente, ma non abbastanza significativo da giustificare una nuova implementazione con i suoi problemi.

Cosa sarebbe necessario per soddisfare tutti e tre i criteri? Non ne abbiamo idea Esistono alcuni schemi di analisi statica che possono estrarre alcune informazioni su tipi concreti, flusso di controllo, ecc. Dai programmi Python. Quelli che producono dati accurati oltre l'ambito di un singolo blocco di base sono estremamente lenti e devono vedere l'intero programma, o almeno la maggior parte di esso. Tuttavia, non puoi fare molto con queste informazioni, tranne forse ottimizzare alcune operazioni sui tipi predefiniti.

Perché? Per dirla senza mezzi termini, un compilatore rimuove la possibilità di eseguire il codice Python caricato in fase di esecuzione (criteri 1 non soddisfacenti) o non fa alcuna ipotesi che può essere invalidata da qualsiasi codice Python. Sfortunatamente, ciò include praticamente tutto ciò che è utile per l'ottimizzazione dei programmi: i globi, incluse le funzioni, possono essere rimbalzati, le classi possono essere mutate o sostituite completamente, i moduli possono anche essere arbitrariamente modificati, l'importazione può essere dirottata in diversi modi, ecc. Una singola stringa passata a eval, exec, __import__o numerose altre funzioni, possono fare qualsiasi di queste cose. In effetti, ciò significa che quasi nessuna grande ottimizzazione può essere applicata, producendo scarsi benefici in termini di prestazioni (fallimento dei criteri 3). Torna al paragrafo precedente.


4

Il problema più difficile è capire che tipo ha tutto in un dato momento.

In un linguaggio statico come C o Java, una volta che hai visto la dichiarazione del tipo, sai cos'è quell'oggetto e cosa può fare. Se viene dichiarata una variabile int, è un numero intero. Non è, ad esempio, un riferimento di funzione richiamabile.

In Python, può essere. Questo è orribile Python, ma legale:

i = 2
x = 3 + i

def prn(s):
    print(s)

i = prn
i(x)

Ora, questo esempio è piuttosto stupido, ma illustra l'idea generale.

Più realisticamente, potresti sostituire una funzione integrata con una funzione definita dall'utente che fa qualcosa di leggermente diverso (come una versione che registra i suoi argomenti quando la chiami).

PyPy usa la compilazione Just-In-Time dopo aver visto cosa fa effettivamente il codice, e questo consente a PyPy di ​​velocizzare molto le cose. PyPy può guardare un ciclo e verificare che ogni volta che il ciclo viene eseguito, la variabile fooè sempre un numero intero; quindi PyPy può ottimizzare il codice che cerca il tipo di fooogni passaggio attraverso il ciclo e spesso può persino eliminare l'oggetto Python che rappresenta un numero intero e foopuò semplicemente diventare un numero presente in un registro della CPU. Ecco come PyPy può essere più veloce di CPython; CPython esegue la ricerca del tipo il più rapidamente possibile, ma nemmeno la ricerca è ancora più veloce.

Non conosco i dettagli, ma ricordo che c'era un progetto chiamato Unladen Swallow che stava cercando di applicare la tecnologia del compilatore statico per accelerare Python (usando LLVM). Potresti cercare su Google Unladen Swallow e vedere se riesci a trovare una discussione sul perché non ha funzionato come speravano.


Unladen Swallow non riguardava la compilazione statica o i tipi statici; l'avvicinarsi alla fine è stato effettivamente il porting dell'interprete CPython, con tutta la sua dinamicità, su LLVM, con un nuovo JIT di fantasia (un po 'come Parrot, o DLR per .NET ... o PyPy, davvero), anche se quello che sono effettivamente finiti fare è stato trovare molte ottimizzazioni locali all'interno di CPython (alcune delle quali sono diventate la linea principale 3.x). Shedskin è probabilmente il progetto a cui stai pensando che ha usato l'inferenza di tipo statico per compilare staticamente Python (sebbene in C ++, non direttamente nel codice nativo).
Abarnert,

Uno degli autori di Unladen Swallow, Reid Kleckner, ha pubblicato una retrospettiva di Unladen Swallow , che potrebbe valere la pena di leggere in questo contesto, anche se in realtà si tratta più di problemi di gestione e sponsorizzazione che di quelli tecnici.
Abarnert,

0

Come dice l'altra risposta, il problema chiave è capire le informazioni sul tipo. Nella misura in cui puoi farlo staticamente, puoi generare direttamente un buon codice.

Ma anche se non puoi farlo staticamente, puoi comunque generare un codice ragionevole, solo in fase di esecuzione, quando ottieni informazioni sul tipo effettivo . Queste informazioni risultano spesso stabili o hanno al massimo alcuni valori diversi per una particolare entità in un determinato punto di codice. Il linguaggio di programmazione SELF ha aperto la strada a molte delle idee sulla raccolta aggressiva dei tipi di runtime e sulla generazione di codici runtime. Le sue idee sono ampiamente utilizzate nei moderni compilatori basati su JIT come Java e C #.

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.