Da Python a C, e ritorno

Eseguire una semplice somma in CPython si sta rivelando più complicato di quel che si poteva immaginare, principalmente a causa del diverso “modello” dei dati manipolati rispetto al linguaggio C (col quale è implementata la macchina virtuale).

CPython, come già detto, gestisce qualunque dato facendo uso di una gerarchia che discende dall’oggetto (struttura in C) PyObject, che si specializza poi in base al tipo (classe) di oggetto modellato.

Ritornando al codice della somma, una volta appurato che entrambi gli oggetti siano di tipo intero (puntatori a strutture PyIntObject per la precisione), è possibile procedere allo svolgimento dell’effettiva operazione, di cui riporto soltanto le righe di codice essenziali:

/* INLINE: int + int */
register long a, b, i;
a = PyInt_AS_LONG(v);
b = PyInt_AS_LONG(w);
i = a + b;
if ((i^a) < 0 && (i^b) < 0)
  goto slow_add;
x = PyInt_FromLong(i);

Come anticipato dal titolo dell’articolo, il primo problema che si presenta è quello di passare dal “mondo Python” a quello del C. La VM di per sé manipola soltanto PyObject, ma alla fine è pur sempre implementata in C, ed è usando questo linguaggio che si devono implementare tutte le operazioni che supporta la gerarchia dei suoi dati.

Nello specifico, un PyIntObject consente di effettuare operazioni con interi a 32 o 64 bit, a seconda dell’architettura del processore. La sua struttura l’abbiamo già vista ed è molto semplice:

typedef struct {
  Py_ssize_t ob_refcnt;
  struct _typeobject *ob_type;
  long ob_ival;
} PyIntObject;

ob_ival rappresenta ovviamente l’intero in questione.

A questo punto passare da Python a C tramite l’uso della macro PyInt_AS_LONG, come si può intuire, risulta essere abbastanza semplice:

#define PyInt_AS_LONG(op)  (((PyIntObject *)(op))->ob_ival)

Si tratta banalmente di estrarre proprio il campo ob_ival di entrambi gli oggetti Python, e copiarlo nelle variabili a e b definite all’interno del blocco.

A questo punto la somma si ottiene immediatamente, come si può vedere dalla riga successiva, ma ottenuto il risultato “grezzo” (memorizzato nella variabile i) si pone il problema di ritornare nel mondo di Python creando un opportuno oggetto che lo rappresenti correttamente.

In teoria sarebbe anch’essa un’operazione semplice: basterebbe creare un nuovo oggetto PyIntObject, e memorizzare nel campo ob_val il valore appena calcolato. In realtà ciò non è immediatamente possibile per un paio di motivi.

Il primo e più importante è che l’operazione avrebbe potuto generare un overflow (o underflow), poiché il tipo long del C non è in grado di rappresentare interi di grandezza arbitraria, ma soltanto quelli consentiti dall’architettura su cui gira la VM (interi a 32 o 64 bit, come già detto).

L’istruzione if serve proprio allo scopo: verificare se ricadiamo in questo caso (che risulta abbastanza raro, per la verità: 32 bit sono sufficienti per la stragrande maggioranza delle operazioni, come c’insegna l’esperienza) e saltare all’etichetta slow_add che si occupa di eseguire il codice più generico che gestisce operator overloading e “promozione” dei risultati (in particolare quelli numerici).

E’ bene sottolineare che questa verifica assume per buona una pratica molto diffusa, ma non standardizzata dal comitato ANSI & ISO: il fatto che un overflow o underflow comporti un cambio di segno del risultato di un’operazione binaria. Questo ha provocato di recente problemi con le ultime versioni del compilatore GCC (sembra soltanto su OS X), com’è stato riportato, e ha richiesto l’aggiunta di un’apposita opzione di compilazione che forza tale assunzione.

A questo punto (e arriviamo al secondo motivo di cui parlavo) se il risultato non ha generato un overflow (quindi un long è sufficiente per rappresentarlo), possiamo finalmente convertirlo in un oggetto PyIntObject facendo uso della funzione PyInt_FromLong che si occupa proprio di effettuare quest’operazione.

Finora abbiamo visto che in CPython vengono utilizzate numerose macro per velocizzare operazioni comuni. Questa volta si fa ricorso a una funzione (definita nel file intobiect.c, che si occupa di gestire gli oggetti PyIntObject), e non a caso:

#define NSMALLNEGINTS		5
#define NSMALLPOSINTS		257
static PyIntObject *small_ints[NSMALLNEGINTS + NSMALLPOSINTS];

PyObject *PyInt_FromLong(long ival)
{
	register PyIntObject *v;
#if NSMALLNEGINTS + NSMALLPOSINTS > 0
	if (-NSMALLNEGINTS <= ival && ival < NSMALLPOSINTS) {
		v = small_ints[ival + NSMALLNEGINTS];
		Py_INCREF(v);
		return (PyObject *) v;
	}
#endif
	if (free_list == NULL) {
		if ((free_list = fill_free_list()) == NULL)
			return NULL;
	}
	/* Inline PyObject_New */
	v = free_list;
	free_list = (PyIntObject *)Py_TYPE(v);
	PyObject_INIT(v, &PyInt_Type);
	v->ob_ival = ival;
	return (PyObject *) v;
}

Non spiegherò riga per riga come funziona, limitandomi a una trattazione di massima, ma comunque sufficiente a comprendere bene i meccanismi che scattano quando c’è da convertire un intero in un PyIntObject.

La definizione delle macro NSMALLNEGINTS e NSMALLPOSINTS, oltre che del successivo vettore small_ints, serve a gestire una cache di PyIntObject già istanziati per rappresentare tutti gli interi compresi fra -5 (NSMALLNEGINTS) e 256 (NSMALLPOSINTS – 1) inclusi, poiché si tratta di valori utilizzati molto spesso e il caching consente di velocizzare l’operazione di conversione in intero, oltre che ottimizzare l’uso della memoria (grazie al riutilizzo degli stessi oggetti facendo ricorso al meccanismo del reference counting).

La parte iniziale di PyIntObject serve proprio a questo: controllare se il risultato (passato come unico parametro) ricade in quest’intervallo, e in tal caso prelevare l’oggetto PyIntObject corrispondente, incrementarne il reference count tramite l’apposita macro Py_INCREF, e restituire immediatamente tale oggetto.

Al contrario se la condizione non è verificata, sarà necessario ricorrere all’apposita trafila prevista per il caso peggiore: allocare memoria per un oggetto di tipo PyIntObject, inizializzare la struttura di base PyObject (quindi i campi ob_ref e ob_type che si occupano rispettivamente di tenere conto del reference counting e del tipo / classe dell’oggetto stesso), memorizzare il valore desiderato e infine restituirlo.

Preciso che tutte queste operazioni sono svolte da una funzione già esistente, PyObject_New, ma per evitare una chiamata a funzione (e ritorno) il suo codice è stato “inglobato” in PyInt_FromLong, sempre per il solito mantra della maggior velocità di esecuzione che permea la VM.

Nel prossimo articolo dedicato a CPython vedremo a grandi linee anche il codice che si occupa di “sommare” (concatenare) due stringhe, e la finalizzazione del codice relativo al bytecode BINARY_ADD.

Press ESC to close