All’ultima EuroPython tenutasi a Firenze ho avuto la possibilità di presentare un talk riguardo a un mio lavoro di ricerca sulla macchina virtuale di Python (chiamata CPython, essendo scritta in C) che trattava di alcune ottimizzazioni possibili sugli interi.
Penso sia interessante capire in che modo lavora internamente, per essere coscienti del peso che possono avere operazioni anche banali, come la classica somma di due numeri, ad esempio, di cui parlerò in questo articolo.
Preciso che parlerò dell’implementazione di CPython 2.6, ma il discorso è di carattere generale e rimane valido anche per la versione 2.7 (l’ultima del ramo 2.x), e per le precedenti. Comunque a grandi linee è applicabile anche ad altre virtual machine (d’ora in poi VM) basate sul concetto di stack.
Python è un linguaggio compilato in una forma astratta chiamata usualmente bytecode anziché in linguaggio macchina, come avviene invece in linguaggi come il C, Delphi, ecc..
In comune c’è comunque la compilazione, cioè la fase che prevede la lettura del sorgente e la generazione di un codice oggetto (che potrebbe anche essere un altro sorgente!), di cui però non ci occuperemo in questa fase per semplificare la trattazione.
Supponendo che il compilatore abbia già fatto il lavoro di compilazione di una semplice funzione che esegue la somma di due parametri:
def f(x, y): return x + y
vediamo come si presenta l’equivalente in bytecode:
2 0 LOAD_FAST 0 (x) 3 LOAD_FAST 1 (y) 6 BINARY_ADD 7 RETURN_VALUE
Al momento non ci addentreremo nei dettagli di più basso livello di questa rappresentazione (ci saranno altre occasioni), ma ci soffermeremo sulle sole istruzioni che vengono eseguite, per vedere concretamente quali passi sono necessari alla VM per portare a compimento quanto le abbiamo chiesto.
La prima istruzione LOAD_FAST serve per prelevare il valore della variabile locale x (cioè il primo parametro della funzione; tutti i parametri vengono inseriti nelle variabili locali, come in altri linguaggi) e inserirlo in cima allo stack utilizzato per la valutazione delle espressioni, che si presenterà così:
------- 2| x | <--- ------- 1| y | ------- 0| x | -------
La cima dello stack (contrassegnata con una freccia) è rappresentata dall’elemento di offset 2, cioè quello appena inserito. Gli altri due elementi rappresentano i due parametri passati alla funzione, che vengono conservati in due variabili locali.
CPython utilizza lo stesso vettore per memorizzare sia le variabili locali che i risultati delle espressioni; ciò si spiega il risultato appena mostrato.
Com’è facile intuire, eseguendo la seconda LOAD_FAST viene prelevato il valore della variabile locale y e inserito in cima allo stack, che assume di conseguenza la seguente forma:
------- 3| y | <--- ------- 2| x | ------- 1| y | ------- 0| x | -------
con l’offeset 3 che adesso ne rappresenta la cima.
A questo punto lo stack contiene tutto ciò che serve, ed eseguendo la successiva operazione, BINARY_ADD, vengono prelevati i due valori dallo stack, sommati, e il risultato infine inserito sempre in cima allo stack:
------- 2| x+y | <--- ------- 1| y | ------- 0| x | -------
A questo punto l’ultima istruzione, RETURN_VALUE, altro non fa che prendere la cima dello stack e restituirla al chiamante, concludendo quindi l’esecuzione della funzione, e lasciando lo stack com’era all’inizio dell’esecuzione della funzione:
------- 1| y | <--- ------- 0| x | -------
Sembra tutto molto semplice, ma questa, alla fine, rimane pur sempre un’astrazione, cioè una rappresentazione di più alto livello di quello che era scritto nel sorgente originale, per cui risulta ben lontana dalle operazioni che verranno poi effettivamente svolte dalla macchina, che sono decisamente numerose a fronte di istruzioni all’apparenza così semplici.
Un’ultima considerazione la merita lo stack. Guardandolo fin dalla prima rappresentazione e, soprattutto, focalizzando l’attenzione sulla seconda, appare evidente che, anche senza le due LOAD_FAST, esso contenga comunque i due elementi che servono per effettuarne materialmente la somma. Anche questa osservazione ha contribuito alla realizzazione del mio progetto WPython, di cui parlerò in futuro.
Nel prossimo articolo, invece, verranno analizzati “al microscopio” i dettagli interni (in C) di questi bytecode, per comprendere meglio le azioni eseguite e cominciando, quindi, a prendere confidenza col mondo della VM.