Uno degli sviluppi hardware più interessanti degli ultimi anni è senza dubbio la penetrazione delle architetture con allineamento a 64 bit (spazio di indirizzamento e registri di calcolo allineati a 64 bit) nel mercato del grande consumo. Architetture di questo tipo chiaramente non sono nuove, erano tuttavia relegate al mondo dei supercomputer. Il passaggio allo spazio commerciale e quindi la vasta disponibilità offre tutta una serie di vantaggi, essenzialmente legati alla possibilità di disporre di uno spazio di indirizzamento incredibilmente esteso. Ma tali vantaggi possono essere sfruttati appieno solo grazie ad alcuni accorgimenti nel disegnare i sistemi software, incluse le JVM. Di certo, un fatto che spesso in prima analisi può sorprendere, è che sistemi disegnati per architetture a 32 bit fatti eseguire su architetture a 64 tendono a subire una riduzione delle performance.
Introduzione
Cominciamo così: siamo in grado di apprezzare praticamente cosa significhi uno spazio di indirizzamento a 64 bit? Le architetture tradizionali a 32 bit, come noto, sono in grado di gestire uno spazio di indirizzamento pari a 4 GByte (232 = 4 * 1K * 1K * 1K), e fin qui nulla di nuovo. Da tener presente che, di questi 4 GB, buona parte viene riservata dal sistema operativo e relativi processi. 4 GByte è un numero tutto sommato gestibile dal punto di vista pratico che non crea grandi problemi.
Ora però, passando ad architetture a 64 bit lo spazio di indirizzamento cresce enormemente fino a raggiungere la dimensione di 264 ossia 18.446.744.073.709.551.615 che equivale a circa 1,845×1019, 18,45 exabyte che, ovviamente, è un numero di non immediata comprensione. Memorie di queste dimensioni, chiaramente, non sono ancora neanche ipotizzabili, almeno per il mercato non militare. Memorie dell’ordine di 8/16 GB per desktop e di 32/64 GB per i server sono decisamente più realistiche (almeno per il momento!).
Per cercare di apprezzare gli ordini di grandezza, si immagini un gigantesco display in cui ogni singolo indirizzo individua univocamente un minuscolo pixel della dimensione di 0,15 mm2. In questo caso, uno spazio di indirizzamento di 4 GB permetterebbe di controllare un gigantesco display della superficie di 4 294 967 296 * 0.015 = 644245094,4 mm2 che equivale a circa a 644 m2. Pertanto si trattererebbe di un display delle dimensioni paragonabili a quelle di un campo da tennis riempito di pixel microscopici.
Rimanendo nell’esempio, un indirizzamento a 64 bit, permetterebbe di indirizzare un display di dimensione pari a 232 campi da tennis (264 = 232 * 232) ossia circa 4 miliardi di campi da tennis! Si giunge così a superfici dell’ordine di grandezza dell’intera Argentina.
Per questioni di completezza vale la pena ricordare che le prime CPU commerciali avevano i registri allineati a 16 bit che equivale ad un “misero” spazio di indirizzamento di 64 KByte. Sempre nell’esempio di riferimento del display, si avrebbe una superficie di 216 = 65536 * 0,15 mm2 = 9830 mm2 = circa 100 cm2: in pratica un foglietto di 10 cm di lato.
Figura 1. Ordine di grandezza dell’indirizzamento di architetture a 16, 32 e 64 bit paragonati nell’esempio a un foglietto di carta millimetrata, a un campo da tennis, a una superficie paragonabile a quella dell’intera Argentina (le immagini, ovviamente, non sono in scala…).
Le CPU a 64 bit più popolari
Architetture a 64 bit sono ormai disponibili a prezzi assolutamente compatibili con il consumo di massa. Vogliamo illustrare brevemente il trend dei nuovi processori senza alcuna pretesa di fare un trattato esaustivo sull’argomento per il quale si rimanda alla letteratura specialistica. I principali produttori mondiali di CPU hanno messo a punto le proprie offerte. Per citarne solo alcuni basti pensare a quelli che seguono.
AMD Opteron [1], si tratta di un processore, più precisamente di una famiglia di processori, rilasciata nel 2003 che rappresenta un po’ il capostipite della serie K8 (ottava generazione di CPU x86) e prima implementazione della tecnologia AMD64 (x86-64). Le sue caratteristiche peculiari, principalmente indirizzamento a 64bit, controller di memoria integrato, tre livelli di cache integrate, lo rendono adatto al mercato dei server e server blade. Le versioni più recenti includono la serie 8 Dual Core, il numero inziale della serie indica la possibilità di poter funzionare a configurazione a 8 processori, dotato di cache L2 4×512 e L3 di 6Mb e il Quad Core. La serie 8 è acquistabile a costi dell’ordine delle migliaia di dollari.
AMD Athlon64 [2], si tratta della versione commerciale dei processori che implementano l’architettura K8 e quindi destinato al mondo dei PC. Le versioni Dual Core si distinguono, tra l’altro, per avere diverse CPU (a partire dalle versioni X2) ognuna dotata della propria cache (L1 e L2), supporto per le istruzioni SSE2, protezione avanzata dai virus, aritmetica in virgola mobile a 128 bit, etc. Il costo di queste versione è dell’ordine delle centinaia di dollari.
Intel EM64T (Intel Extended Memory 64 Technology) [3]. Nocona è il nome in codice del primo (core) processore Intel che implementa questa tecnologia ed è stato introdotto nei microprocessori della Xeon. Quindi tutti i modelli Pentium 4.6xx, Pentium 4 5×1 e Celeron D 3×1 / 3×6 dispongono della tecnologia EM64T. Ciò significa che tutte le più recenti CPU Intel sono dotate di architetture a 64bit. I prezzi variano, a seconda del modello, da alcune centinaia fino alle migliaia di dollari. Anche in questo caso, per rendersi conto della complessità di questi microprocessori è sufficiente pensare che sono costruiti utilizzando 125 milioni di transistor che danno luogo alla core CPU che implementa, tra l’altro le tecnologie MMX, SSE/2/3 e (ovviamente) EM64T e che ospita una cache di livello 2 di 1MB e 16KB di cache L1. La frequenza interna arriva a 800Mhz, che equivale 6.4GB/sec di larghezza di banda.
La disponibilità delle versioni commerciali di questi processori significa che presto la maggior parte degli utenti di PC avrà sulle proprie scrivanie, sia di casa sia di ufficio, sistemi a 64 bit corredati da applicazioni appositamente studiate per tali sistemi in grado di modificare significativamente l’esperienza utente. Molti programmi, per esempio, tenderanno a funzionare più velocemente grazie alla disponibilità di maggiori quantitativi di memoria che permettono di minimizzare gli accessi al disco rigido. I programmi multimediali e, più in generale quelli CPU-intensive come i vari CAD, potranno trarre enormi vantaggi dal nuovo spazio di indirizzamento, mentre, probabilmente, il programma delle fatture non presenterà particolari miglioramenti.
Quindi, per architetti e programmatori è importante comprendere le potenzialità delle architetture a 64bit al fine di sfruttarne appieno i vantaggi.
Misure
Sebbene come al solito Internet non lesini blog dedicati all’argomento, all’inizio solo pochi esperti avevano compreso che l’esecuzione as-it-is di applicazioni disegnate per architetture a 32bit su architetture a 64 avrebbe inevitabilmente generato perdite di performance. Ora ciò è diventato un fatto acquisito tanto che, per esempio fin dal JDK6, è stata introdotta nella JVM la possibilità di comprimere i riferimenti in memoria, come vedremo in seguito.
Analisi quantitative dettagliate sull’argomento non abbondano: tuttavia è disponibile un ottimo studio in questa direzione condotto da Kris Venstermans, Lieven Eeckhout e Koen De Bosschere “64-bit versus 32-bit Virtual Machines for Java” [4].
In particolare, sono state eseguite delle indagini approfondite su due versioni della JVM, rispettivamente: the Jikes Research VM and the IBM JDK 1.4.0 production VM che hanno la peculiarità di poter funzionare in entrambe le modalità (32 e 64 bit) sullo stesso hardware. Gli aspetti di particolare interesse dell’indagine, come lecito attendersi, sono stati i classicissimi “occupazione di memoria” e “performance”. Lo studio ha evidenziato una serie di aspetti molto interessanti:
- lo spazio di memoria (heap) occupato dagli stessi oggetti tende a crescere di un fattore vicino al 40% facendo funzionare la medesima applicazione Java su una macchina virtuale a 64 rispetto a una a 32 bit;
- vi è una generale riduzione delle performance. Questa può essere minimizzata e ridotta ad un fattore pari a 1,7% aumentando però del 40% lo spazio di memoria heap allocato all’applicazione. Qualora ciò non avvenga, non solo si assiste a un consistente degrado delle performance, ma anche alla presenza di condizioni di crash dovute all’esaurimento della memoria (OutOfMemoryError).
Sebbene questi dati in prima analisi possano sembrare sorprendenti, riflettendoci bene si capisce come in fondo tutto ciò sia assolutamente logico ed è ovviamente dovuto al notevole incremento del data footprint dell’applicazione. In particolare, la maggiore occupazione di memoria è dovuta al fatto che:
- i riferimenti in memoria (quelli che una volta venivano chiamati puntatori), occupano esattamente il doppio dello spazio;
- l’header degli oggetti richiede più spazio (lo spazio tipicamente assegnato all’header di un oggetto in architetture a 32 bit è dell’ordine 64 bit a cui, normalmente, viene aggiunto altro spazio per gli array necessario per contenerne la dimensione; tale spazio tende a raddoppiarsi in architetture a 64 bit);
- ci sono byte di memoria sprecati per dover garantire l’allineamento;
- c’è maggiore occupazione di memoria dello stack.
L’aumento di spazio finisce per aumentare i refresh delle cache interne della CPU (tecnicamente il numero di miss), ai diversi livelli di gerarchia (L1, L2 e L3). Ecco quindi spiegata anche la diminuzione delle performance.
Una logica conseguenza provata dallo stesso studio, è che anche lo spazio heap richiesto per ottimizzare le performance cresce in maniera proporzionale (40%).
Da tenere presente che sebbene i chip di memoria non siano particolarmente costosi (entro certi limiti), lo stesso non è vero per le cache interne e la larghezza di banda interna.
La tabella 1 mostra l’elenco degli applicativi utilizzati come benchmark, con una serie di considerazioni.
Tabella 1. Lista dei software utilizzati come benchmark.
Le cache dei microprocessori
Nel corso di questo articolo più volte si sono menzionate le cache L1, L2 e L3. Si tratta di memorie [6] estremamente veloci e di dimensioni relativamente contenute interposte tra il processore e la RAM al fine di aumentare la velocità di esecuzione del processore minimizzando gli accessi esterni. Compito di queste cache, come al solito, è di anticipare le richieste di dati e istruzioni dei processori super veloci al fine di evitare che queste richieste debbano essere risolte dalla RAM molto lente rispetto ai tempi di esecuzione dei processori. Per essere precisi, alcune di queste sono incorporate direttamente all’interno dei microprocessori, altre nella scheda madre (non era infrequente il caso in cui la cache L3 veniva installata nella mother board come cache condivisa da parte di più processori). La scelta di quante e quali cache utilizzare e dove ubicarle dipende sia dalla specifica famiglia di processori, sia dalla particolare versione. Per esempio è legittimo attendersi che processori disegnati per funzionare su server abbiano diversi processori core integrati (embedded), un numero superiore di cache, e che queste siano di dimensioni e performance superiori. Le cache L, possono essere catalogate come spiegato di seguito.
L1: questa è la cache più interna, integrata direttamente nei processori core. Pertanto, nelle CPU multi-core ogni processore core dispone della propria cache L1. Le sue caratteristiche tipiche consistono nell’essere di dimensioni ridotte, tipicamente nell’ordine dei KB, e nel poter funzionare con performance superiori. La tecnologia normalmente utilizzata è la ultra veloce static RAM (ultra-speed SRAM) in grado di fornire prestazioni decisamente superiori (tipiche delle memorie di dimensioni ridotte) rispetto alle più economiche RAM dinamiche. Le architetture più recenti tendono a disporre di due cache L1, una disegnata per ospitare le istruzioni e l’altra per i dati.
L2: questa tipologia di cache è interposta tra la cache L1 e la L3 o RAM a seconda delle configurazioni. Si tratta di memorie di capacità superiori alle precedenti (tipicamente dell’ordine dei MB) ma con performance e costi inferiori. Nei processori di ultima generazione anche la cache L2 è incorporata direttamente nel processore core. Tuttavia non è infrequente il caso in cui sia integrata nel processore e condivisa tra i vari processori core, mentre è ormai assolutamente infrequente il caso in cui sia inserita nella scheda madre.
L3; quando presente, è anteposta alla RAM. Si tratta di una cache di dimensioni relativamente grandi e con performance inferiori. Inizialmente veniva installata sulla scheda madre, mentre più di recente è introdotta nei processori a più core che la condividono.
Alcuni esempi sono l’IntelCore 2 Quad in cui l’architettura prevede che ogni processore core sia dotato della propria cache di primo livello, e che ci siano due cache L2 ognuna condivisa da due processori core. L’AMD Phenom II X4, Intel Core i5, i7 prevede un’architettura in cui ogni processore core dispone di due livelli di cache (L1 e L2) e tutti i quattro core condividano una cache di Livello 3.
L’efficacia delle memorie cache è misurata dallo hit rate, ossia dal numero di richieste (hit) soddisfatte. Quando una cache non è in grado di soddisfare una richiesta (in questo caso si parla di miss, “mancata”), questa è passata alla memoria di livello successivo. Chiaramente l’obiettivo della cache è di massimizzare la hit rate minimizzando le miss in quanto (ovviamente) lente giacche’ causano l’arresto del ciclo di esecuzione e forzano il processore ad attendere in wait state. Da notare che i dati non sono solamente letti dalle cache ma anche scritti, e quindi anche la strategia utilizzata per memorizzare tali dati nella RAM gioca un ruolo importante. In particolare, si può scegliere tra due principali alternative: aggiornamento simultaneo (write-through) o posticipato (write-back). Come conclusione di questo paragrafo è importante ricordare che man mano che ci si allontana dal processore, le cache disponibili presentano performance e quindi costi inferiori e, in compenso aumentano di dimensione.
Contromisure
Appurato che sistemi non disegnati per sfruttare appieno le capacità delle architetture a 64 bit presentano una degradazione delle performance una volta fatte eseguire su di queste e che l’unica possibilità di mitigazione consiste nell’aumentare le risorse a disposizione (soprattutto la memoria), si è posto il problema di trovare una soluzione anche al livello di JVM per i sistemi scritti in Java. Questa soluzione prende il nome di “compressione dei puntatori a oggetti” (object compressed pointers) introdotta nella Java HotSpot VM 14.0 (JDK 1.6.0_14).
Le note ufficiali includono la seguente descrizione:
-XX +UseCompressedOops è un’opzione che permette di migliorare le performance del Java Runtime Environment a 64 bit quando l’applicazione Java utilizza meno di 32 GB di spazio heap -scenario tipico delle applicazioni disegnate per architetture a 32 bit-. In questo caso, HotSpot comprime le referenze agli oggetti a 32 bit, riducendo quindi lo spazio che deve processare.
Il nome di questa feature è dovuto al fatto che l’acronimo “oop” nel contesto del HotSpot è utilizzato per indicare Ordinary Object Pointer (“puntatore ordinario ad oggetto”). La dimensione di tali puntatori è normalmente data dalla dimensione dell’architettura dove gira la JVM, quindi 32 bit su architetture a 32 bit e 64 su quelle a 64.
Come visto nei paragrafi precedenti, un’applicazione progettata per un’architettura a 32 bit per funzionare così com’è su architetture a 64 richiede uno spazio heap del 40% più grande per poter fornire performance paragonabili. Sebbene il trend tecnologico sia quello di produrre chip di memoria di dimensioni sempre crescenti a prezzi sempre inferiori, lo stesso purtroppo non avviene per le cache interne ai microprocessori e per la relative larghezza di banda. Quindi, allo stato attuale, la piena espansione a 64bit non è così semplice come si potrebbe pensare. Pertanto i puntatori compressi rappresentano una strategia per consentire alla JVM di operare con indirizzi a 32 bit su architetture a 64. La strategia impiegata è, dal punto di vista teorico, molto semplice e consiste nell’utilizzare riferimenti relativi a 32 bit che a partire da un indirizzo di base puntano ad uno spazio di memoria contiguo. La traduzione degli indirizzi compressi richiede di aggiungere un offset a 32 bit per ottenere quindi l’indirizzo a 64. La procedura inversa, compressione degli indirizzi, si ottiene sottraendo l’offset all’indirizzo a 64 bit e quindi utilizzare i 32 bit meno significativi. Questo però è vero solo da un punto di vista teorico: in realtà la situazione è molto più complessa, come si vedrà nel corso del prossimo articolo.
Conclusioni
In questo articolo si è analizzato l’impatto sui sistemi software della penetrazione delle architetture a 64 bit nel mercato di massa. L’innovazione principale è indubbiamente legata alla possibilità di gestire spazi di indirizzamento enormi (almeno per gli standard attuali!). Tale possibilità tuttavia non è esente da problemi. In effetti, il passaggio degli indirizzi da 32 a 64 bit finisce per generare importanti ripercussioni sia sul memory foot print delle applicazioni, sia sulle performance. Gli studi disponibili mostrano che la medesima applicazione implementata per architetture a 32 bit fatta funzionare as it is in una architettura a 64 presenta un incremento del memory foot print stimabile nell’ordine di grandezza del 40%. La logica conseguenza è che lo heap allocato per l’applicazione deve essere incrementato conseguentemente al fine di evitare i problematici OutOfMemoryError. Con tale incremento le performance tendono comunque a subire un degrado dell’ordine dell’1,5% (questi dati sono stati ricavati eseguendo le applicazioni di benchmark). Da tenere presente che, sebbene l’acquisto di memorie RAM di dimensioni sempre maggiori non sia un grande limite, lo stesso non si può dire per le cache interne dei processori (le famose L1, L2 e spesso L3) e per la corrispondente bandwith. Questo fenomeno è ben noto nella comunità Java, tanto è vero che già dalla versione JDK6 è stata introdotta la feature dei puntatori compressi.
Visti e considerati tutti questi problemi, la domanda da porsi è se valga veramente la pena di ricorrere ad architetture a 64bit, e, in caso affermativo, quali ne siano i vantaggi. Bene… si risponderà a queste domande nel corso del prossimo articolo.
Riferimenti
[1] AMD Opteron™ Processor Solutions
http://products.amd.com/en-us/OpteronCPUResult.aspx?f1=Third-Generation+AMD+Opteron%e2%84%a2
[2] AMD Athlon™ II Processors
[3] Products (Formerly Nocona)
http://ark.intel.com/ProductCollection.aspx?codeName=1789
[4] Kris Venstermans, Lieven Eeckhout, Koen De Bosschere, “64-bit versus 32-bit Virtual Machines for Java”
http://users.elis.ugent.be/~leeckhou/papers/SPE06.pdf
[5] Changes in 1.6.0_14 (6u14)
http://www.oracle.com/technetwork/java/javase/6u14-137039.html
[6] Athlon II Or Phenom II: Does Your CPU Need L3 Cache?
http://www.tomshardware.com/reviews/athlon-l3-cache,2416.html