Emulazione, simulazione, e limiti tecnologici

Il precedente articolo sul MAME ha scatenato un po’ di polemiche che francamente non mi sarei aspettato, per cui ho deciso di affrontare alcuni argomenti che sono venuti fuori, in particolare quello prettamente tecnico che riguarda il cieco affidarsi all’evoluzione della tecnologia per giustificare il ricorso a codice sempre meno ottimizzato e/o esageratamente spostato verso la più onerosa simulazione piuttosto che alla più leggera emulazione.

Premetto intanto che l’emulazione era pre-esistente al MAME, da un bel pezzo. La prima volta che ne ho sentito parlare era ai tempi dell’Amiga, col suo emulatore Commodore 64 (per chi se lo ricorda). MAME dalla sua ha avuto il vantaggio non indifferente di aver messo assieme diversi sistemi arcade (anche da emulatori già esistenti), che all’epoca avevano ancora un enorme successo, divenendo il punto di riferimento nel panorama dell’emulazione.

E’ stato detto che l’obiettivo del team, dichiarato dallo stesso autore, è di perseguire la “preservazione” dei videogiochi originali, privilegiando l’accuratezza alla velocità di esecuzione. Una tendenza alla perfezione, quindi, senza compromessi. Ma, come già anticipato, l’emulazione perfetta, in cui il funzionamento interno del sistema viene riprodotto fedelmente, si chiama in realtà simulazione, ed è un’altra cosa.

In ogni caso gli sviluppatori hanno fatto ampio uso di High Level Emulation (HLE), e in alcuni casi proprio per questioni prestazionali (come vedremo dopo). D’altra parte l’approccio classico è quello di scrivere il prima possibile il driver per un sistema, per poi “raffinarlo” migliorando via via la qualità dell’emulazione.

Il problema del MAME è che si tratta di una progettazione a senso unico: gli “scarti” (codice HLE) vengono prima o poi abbandonati in favore dell’unica soluzione possibile per loro: quella finale, che risulta maniacalmente “aderente” all’originale.

Alcune tecniche HLE rimangono, però, in piedi proprio per garantire una buona velocità di esecuzione, perché la fedele riproduzione del funzionamento interno del sistema risulterebbe talmente avida di risorse da rendere l’emulazione (simulazione) praticamente inutilizzabile.

Un chiaro esempio lo si trova qui, dove spiega come viene implementata la gestione della TLB (Translation Lookaside Buffer), una veloce cache utilizzata dai processori originali per memorizzare gli (ultimi, generalmente) indirizzi virtuali già tradotti. In particolare:

Unfortunately, the details of TLB operation are often not fully disclosed, and even if they are, they are not quite as efficient to emulate in software.

L’ultima frase dovrebbe essere abbastanza eloquente, e giustifica l’approccio utilizzato dal MAME in questi casi, volto a privilegiare la velocità di esecuzione rispetto all’accuratezza dell’emulazione. Quindi preservazione sì, ma fino a un certo punto: la velocità d’esecuzione, checché se ne dica, rimane importante, se non si vuol vedere scorrere le immagini prodotte al ritmo di frame per minuto anziché per secondo…

Ci si potrebbe chiedere a questo punto perché non affidarsi ai progressi della tecnologia anche in questi casi. D’altra parte è il mantra che viene spesso ripetuto in queste circostanze, quando cioè si additano le nuove versioni del MAME come troppo lente per eseguire un gioco come Frogger (su un sistema che è però mille volte più “potente”).

Lo si è letto in diversi commenti, e lo stesso moderatore che ha chiuso il ticket oggetto del precedente articolo ha suggerito un aggiornamento per quello che considera un sistema troppo vecchio. Se lo dicono in tanti, anche persone che possono godere di una certezza autorevolezza, potrebbe anche essere vero, e la strada indicata quella giusta. E’ un approccio abbastanza comune, ma poco logico, che verte sulla fiducia più che su fatti concreti.

E’ vero che più passa il tempo e più potenza di calcolo abbiamo a disposizione, peraltro anche a costi ridotti, ma ciò non è sufficiente per cullarsene dimenticando come funziona un emulatore e quali esigenze deve risolvere. Questa mentalità è, anzi, particolarmente pericolosa, in quanto invoglia a prendere delle scelte sulle quali sarà poi difficile tornare indietro (se non a costi elevati).

Bisogna rendersi conto che la tecnologia ha subito un grosso stop in termini di frequenze raggiunte e di IPC: su entrambi i fronti si avanza ormai molto lentamente, e il motivo è riconducibile ai limiti intrinseci della tecnologia e della “computazione” rispettivamente.

La corsa ai Ghz, portata in pompa magna da Intel con l’arcinota famiglia dei Pentium 4, quale soluzione ai problemi prestazionali, ha condotto a un rovinoso baratro una roadmap che prevedeva già qualche anno addietro processori da ben 10Ghz. La continua riduzione della dimensione dei transistor ha, infatti, portato alla ribalta fenomeni fisici coi quali i produttori di chip hanno dovuto fare i conti, lasciando la frequenza ai piedi dei 4Ghz.

Si è preferito tornare a puntare sull’efficienza, al contrario dei P4 che non erano stati pensati allo scopo quanto per scalare in frequenza appunto. Efficienza che spesso si misura in IPC (Instructions Per Cycle), cioè mediamente quante istruzioni vengono eseguite dal microprocessore per ogni ciclo di clock.

Anche l’IPC ha dei limiti intrinseci strettamente legati alla computazione. Non è possibile, infatti, aumentare a dismisura questo valore, poiché il codice è soggetto a vincoli di dipendenza che, di fatto, renderebbe del tutto vana la possibilità di poter eseguire un numero elevato di istruzioni per ciclo di clock, a fronte di un’enorme complicazione a livello architetturale, che porterebbe comunque a frequenze meno elevate, consumi spropositati, e costi eccessivi.

Se soltanto negli ultimi anni abbiamo visto CPU (x86, in particolare) dotate di decoder in grado di decodificare fino a 4 istruzioni per ciclo di clock anziché 16 (tanto per sparare un numero a caso), è proprio perché decodificare tante istruzioni “subito”, che verranno poi eseguite chissà quando, non è una scelta molto intelligente, in quanto il decoder è uno dei componenti più costosi e critici (come abbiamo già discusso tempo fa proprio un queste pagine).

Con frequenze e IPC aventi una crescita limitata o sostanzialmente bloccata, i produttori di CPU hanno preferito puntare tutto sull’elaborazione parallela, prima con tecnologie come Hyperthreading (nate principalmente per sfruttare il più possibile le unità di calcolo che sono troppo spesso lasciate inoperative) e poi con core completi (che non condividono risorse con altre parti, se non per il bus e/o le cache L2/L3).

Un approccio, questo, che mal si sposa con l’emulazione, specialmente se dev’essere accurata, poiché si tratta di un processo strettamente sequenziale che sfrutta generalmente un solo core / processo / thread per l’emulazione della macchina vera e propria, facendo eventualmente uso di qualche altro thread per la sincronizzazione dell’I/O (grafica, audio, input) col sistema operativo (ma si tratta pur sempre di ben poca roba).

Lo possiamo leggere meglio nella pagina che è stata scritta per descrivere in che modo viene portata a compimento l’esecuzione in un sistema che deve emulare più CPU (o, in generale, dei “device“, come ad esempio dei coprocessori dedicati).

In soldoni, ciò significa che frequenza e IPC diventano i parametri fondamentali coi quali confrontarsi quando si parla di emulazione; parametri che sono purtroppo intrinsecamente limitati, e per i quali ormai i progressi della tecnologia non promettono più scalate all’inseguimento della famosa legge di Moore.

Si potrebbe pensare di sfruttare i numerosi core, in modo da assegnare a ciascuno di essi uno specifico processore da emulare. E’ un’idea che affiora in maniera piuttosto naturale, ma che non è applicabile concretamente. Bisogna considerare che già adesso prendere un algoritmo e “parallelizzarlo” presenta notevoli problematiche da risolvere. Intanto per la natura stesso del problema, che potrebbe non essere “parallelizzabile”. E poi per la sincronizzazione dei vari core che bisogna coordinare al fine di ottenere il risultato desiderato, cosa tutt’altro che semplice e scontata.

Scendendo nello specifico, l’emulazione di più core deve prevedere già di per sé la sincronizzazione dei vari componenti (in base alla particolare architettura del sistema) che non è cosa semplice (e, soprattutto, performante) da realizzare con un solo core/processo, e a cui si aggiungerebbero i problemi di sincronizzazione dei core fisici che se ne dovrebbero occupare. Ecco spiegato il motivo per cui il MAME emula un dispositivo alla volta, cedendo la CPU per un tot di tempo a ognuno di essi.

A conti fatti avere un PC con tanti core non serve praticamente a nulla, e poiché la direzione della tecnologia è quella di avere CPU sempre più potenti, ma perché dotate di molti core, si capisce bene che aggrapparsi all’avanzamento tecnologico per soddisfare i requisiti prestazionali sempre più elevati di progetti come il MAME, diventa sempre più un vicolo cieco in cui ci si infila senza, però, poterne più uscire.

Perché nel momento in cui alzi l’asticella e si pretendono sistemi più avanzati per avere un’emulazione (o simulazione), di fatto ci si è gettati la zappa sui piedi. Con buona pace delle “buone” intenzioni con cui vengono elargiti, spesso in buona fede, questi “consigli”.

Press ESC to close