Tiriamo le somme in CPython

Dopo un’occhiata al funzionamento “di massima” della macchina virtuale implementata con CPython e analizzato in dettaglio le azioni che scattano nel momento in cui abbiamo deciso di utilizzare il valore memorizzato nella variabile locale per farci qualcosa, entriamo nel concreto con l’esempio visto inizialmente e che riguarda una semplice somma.

Il blocco di codice che in CPython (ricordo che parliamo della versione 2.6.4, ma il discorso è valido per le versioni 2.x e, per buona parte, per le 3.x) è deputato all’espletamento dell’operazione di “addizione” si presenta decisamente imponente rispetto a quanto visto finora, e mette anche un po’ soggezione:

case BINARY_ADD:
  w = POP();
  v = TOP();

  if (PyInt_CheckExact(v) && PyInt_CheckExact(w)) {
    /* 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);
  }

  else if (PyString_CheckExact(v) &&
           PyString_CheckExact(w)) {
    x = string_concatenate(v, w, f, next_instr);
    /* string_concatenate consumed the ref to v */
    goto skip_decref_vx;
  }

  else {
    slow_add:
      x = PyNumber_Add(v, w);
  }

  Py_DECREF(v);
 skip_decref_vx:
  Py_DECREF(w);
  SET_TOP(x);
  if (x != NULL) continue;
  break;

Ho provveduto ad aggiungere delle righe vuote per separare logicamente il codice in parti che hanno ognuna delle precise responsabilità, in modo da semplificarne la trattazione.

La somma è un’operazione binaria, per cui per poter lavorare e produrre il risultato ha ovviamente bisogno di due operandi, che vengono prelevati dallo stack (CPython implementa una virtual machinestack-based“) tramite la macro POP che risulta molto semplice (d’altra parte è la simmetrica della PUSH che abbiamo visto in precedenza):

#define POP()  (*--stack_pointer)

Quindi lo stack che Python usa per memorizzare le variabili locali e i risultati intermedi passerebbe da:

 -------
3|  y  | <---
 -------
2|  x  |
 -------
1|  y  |
 -------
0|  x  |
 -------

a:

 -------
1|  y  | <---
 -------
0|  x  |
 -------

Infatti questa era la situazione iniziale in cui si presenta appena prima di eseguire la prima istruzione bytecode, mentre quella precedente riflette le due operazioni LOAD_FAST che hanno inserito nello stack rispettivamente il contenuto della variabile python x e della y, come visto nel precedente articolo.

Il primo POP rimuove il valore di y (che si trova in cima) e lo pone nella variabile w, mentre il secondo fa lo stesso, ma per il valore di x che finisce in v.

v e w sono due variabili di comodo (puntatori a strutture di tipo PyObject) definite all’interno della funzione che esegue il ciclo principale della VM, e che vengono utilizzate per gli scopi più disparati.

La seconda parte dimostra la natura dinamica di Python: abbiamo due valori che sono “istanze” di una qualche “classe”, e in questo contesto dobbiamo capire cosa fare quando incontriamo il simbolo +, che non serve esclusivamente per sommare numeri, com’è evidenziato già dal primo if.

Python mette a disposizione diversi tipi di dati nativi e consente anche di crearne altri permettendo, tra l’altro, di poter definire cosa fare per determinate operazioni come la “somma” (il classico concetto di operator overloading, che suona sicuramente meglio di “sovraccarico degli operatori”).

Nello specifico, una delle operazioni più frequenti è proprio la somma fra interi (tipi “primitivi” / predefiniti di Python), ed è il motivo per cui il primo controllo verte proprio su di essi:

#define PyInt_CheckExact(op) ((op)->ob_type == &PyInt_Type)

Pur non essendo scritta tutta in maiuscolo, com’è abitudine fare in C in questi casi, PyInt_CheckExact è una macro che agisce su un puntatore a una struttura PyObject, e restituisce un valore booleano.

Sappiamo già che tutti gli oggetti di CPython sono costituiti da una struttura “base” (PyObject) che definisce due campi chiamati ob_ref e ob_type, che rappresentano il numero di volte che l’oggetto risulta “referenziato” e il tipo dell’oggetto (sotto forma di puntatore a un’ulteriore struttura dati).

In pratica per poter dire che un oggetto è di un certo tipo o, equivalentemente, che appartiene a una certa classe, si rende necessario legarlo a essa in qualche modo, ed è quello che fa il campo ob_type, appunto.

Poiché in Python anche i tipi e le classi (che sono esattamente la stessa cosa) sono oggetti, è facile intuire che ob_type sia un puntatore a… PyObject.

Infatti PyInt_Type è una variabile (opportunamente inizializzata per “modellare” la classe degli interi) in realtà di tipo PyTypeObject, che però a sua volta “discende” da PyObject. Non ne riporto la definizione perché non serve allo scopo del presente articolo, ma in futuro salterà fuori e sarà necessario per capire alcuni aspetti di più basso livello di CPython.

Al momento, quindi, prendiamo per buono che PyInt_Type rappresenti la struttura del tipo intero standard di CPython, per cui per verificare se un oggetto PyObject è di questo tipo, risulta sufficiente confrontare il suo ob_type col puntatore a PyInt_Type (espressione &PyInt_Type), esattamente come fa la macro PyInt_CheckExact.

Adesso che sappiamo distinguere se un oggetto è un tipo intero (primitivo) di CPython oppure no, possiamo procedere a esaminare il rimanente pezzo di codice della seconda parte (che si occupa della somma vera e propria di due interi) se la condizione è verificata per entrambe le variabili v e w, come vedremo meglio nel prossimo articolo.

Press ESC to close