Schede di approfondimento: Architetture evolute Le tecniche utilizzate per migliorare le prestazioni dei sistemi di calcolo, partono dal semplice aumento della frequenza del clock ed attraverso l'introduzione delle cache, della pipeline, della tecnologia RISC, giungono fino alle ultime tecniche di integrazione tra RISC e CISC. Aumento della velocità Velocità dei procesori Intel in Milioni di Istruzione Per Secondo Gordon Moore ha stabilito nel 1975 una legge, detta legge di Moore, la quale recita che il numero di transistor su ogni singolo integrato raddoppia ogni 18 mesi. Da questa affermazione deriva che la potenza di calcolo raddoppia ogni 18 mesi. Dall'introduzione del primo microprocessore si può rilevare un continuo aumento della velocità, che va in due direzioni non antitetiche. Da una parte vediamo che è aumentata la dimensione dei dati trattati con un'unica istruzione, partendo dai quattro bit delle prime CPU (vedi INTEL 4004) fino ai trentadue bit delle architetture correnti oggi fino ai sessantaquattro bit delle architetture oggi innovative. Questo aumento del numero di bit si traduce in un aumento di velocità in quanto da una parte si è in grado di trattare dati più complessi con singole istruzioni e dall'altra si è in grado di leggere più dati con un'unica operazione di lettura. L'altro tipo di aumento di velocità si è ottenuto incrementando semplicemente la velocità del clock delle CPU, vale a dire riducendo il tempo per eseguire un'istruzione a parità di complessità dell'istruzione stessa (a parità di cicli). Anche in questo caso possiamo vedere un aumento della velocità del clock da 108 KHz del 4004 nel 1971 fino ai 300/400 MHz delle CPU dei giorni nostri, con un aumento del 38% circa ogni anno, anche se l'aumento non è stato affatto costante. Un altro filone per l'aumento della velocità delle CPU consiste nel cercare di ridurre il numero di cicli di clock necessari per ogni singola istruzione. Naturalmente questo numero dipende in primo luogo dalla complessità delle istruzioni stesse, quindi se una CPU introduce un'istruzione molto potente ma anche complessa, questa richiederà molti cicli per essere eseguita. D'altronde, aumentando la complessità dei circuiti interni si è cercato di ridurre il numero di cicli necessari per le istruzioni semplici. Evoluzione1 delle Architetture a Microprocessore • Se le CPU hanno aumentato la loro velocità di 1000 volte, le memorie solo di 3 • Inizialmente la memoria era più veloce del processore • Quando il processore ha superato la velocità della memoria, si sono introdotte le code di prefetch o • Le code possono compensare piccole differenze di velocità, perché quando il processore "esegue" non usa la memoria Per differenze più grandi si deve ricorrere alle memorie cache Memoria cache 1 • Una memoria cache è una piccola2 memoria veloce, che si interpone nel percorso dei dati tra la RAM (DRAM, lenta) e la CPU • La memoria cache è una RAM (SRAM) veloce quanto la CPU (o poco meno) • Nella memoria cache vengono immagazzinati i dati (o istruzioni) usati più di recente • Quando la CPU richiede un dato, se esso è contenuto nella cache, viene restituito rapidamente, se non c'è, occorre aspettare il tempo della RAM http://www.itis.mn.it/inform/evarchi/indexb.htm con approfondimento http://www.itis.mn.it/inform/evarchi/index.htm Memoria di limitata capacità (es. 64KB) in molti casi presente internamente al microprocessore stesso. La prima volta che il microprocessore carica dei dati dalla memoria centrale, tali dati vengono caricati anche sulla cache. Successivamente, i dati vengono letti dalla cache invece che dalla memoria centrale più lenta. Sfrutta le caratteristiche di località spaziale e temporale tipica dei programmi. Problemi di coerenza dei dati contenuti nella cache. 2 • Nella cache, oltre ai dati vanno anche immagazzinate altre informazioni (dette TAG), realtive all'indirizzo originale dei dati contenuti • I TAG sono costituiti da celle di memoria che memorizzano gli indirizzi delle celle dati loro associate • Ogni cella dei TAG contiene, oltre alla memoria, anche un comparatore che confronta l'indirizzo memorizzato con quello prodotto dalla CPU. • Se i due indirizzi coincidono (accadrà al massimo per una calla) si genera un HIT e la cache fornisce il dato • Se l'indirizzo della CPU non corrisponde con nessun TAG, si genera un MISS ed il dato deve essere prelevato dalla memoria • Una memoria come quella dei TAG, con i comparatori associati viene detta memoria associativa. • Se vogliamo che ogni locazione (o ogni "word") di RAM possa andare in ogni cella di cache, ogni cella di TAG deve avere la dimensione dell'address bus o poco meno. • Se ogni locazione di RAM può andare in ogni cella di cache, dobbiamo aggiungere un meccanismo per scegliere la cella di cache da usare • Se vogliamo scartare il dato usato meno di recente, dobbiamo aggiungere una memoria delle età, associata ad ogni cella, che ne indichi la più vecchia. • Una cache organizzata così prende il nome di fully associative • Una cache fully associative ha una quantità di bit di servizio paragonabile con la quantità di bit di dati • Se in un certo periodo utilizziamo anche poche celle in più della capacità della cache, ripetutamente, la cache viene ricaricata completamente ad ogni ciclo. • Se decidiamo che la cache possa immagazzinare un blocco di memoria, come TAG basta il numero del blocco tra i blocchi che costituiscono la memoria • Per rendere più flessibile la cache, possiamo associare questo TAG non all'intera memoria ma ad ogni singola cella • Una cache così organizzata prende il nome di direct mapping • Non servono più le memorie delle età, in quanto ogni cella di RAM può andare in una sola cella di cache • Il numero dei bit aggiunti al dato cala considerevolmente • Se i dati che usiamo eccedono la dimensione della cache, ad ogni ciclo verranno ricaricati solo i dati eccedenti • Se usiamo programmi con subroutine o dati sparsi, è facile che due dati debbano andare nella stessa cella ad ogni ciclo, venendo quindi riletti dalla RAM • Per risolvere il problema dei dati sparsi o delle subroutine possiamo spezzare la cache in più parti, messe in parallelo • In questo modo, più dati che "competono" per la stessa cella di cache, possono andare in "fogli" diversi della cache • Per scegliere il "foglio" servirà una memoria delle età, ma molto piccola, proporzionata al numero di "fogli" • La diminuzione della dimensione del "foglio" comporta anche un leggero aumento della dimensione dei TAG • Una cache di questo tipo prende il nome di set associative • È il miglior compromesso tra fully associative e direct mapping • Per il momento si è ragionato sulla lettura. Se prendiamo in considerazione la scrittura si pongono due casi: o Si può non usare la cache per la scrittura (write through), ma aggiornarla soltanto: la scrittura sarà lenta quanto la RAM o Si può scrivere nella cache (write back): la scrittura sarà veloce come la cache, ma si dovrà riscrivere il dato modificato in RAM prima di scartarlo dalla cache. • Nel caso di architetture multiprocessore dovremo anche preoccuparci che i dati in cache non siano resi obsoleti da scritture in RAM da parte di altri processori. • Per fare questo dovremo utilizzare dei protocolli che controllino l'attività del bus • Questi protocolli prendono il nome di protocolli di coerenza Introduzione del parallelismo (pipeline) • Per aumentare la velocità della CPU possiamo spezzare l'esecuzione delle istruzioni in fasi indipendenti da eseguire in parallelo • Le fasi (eseguibili già in parallelo) tradizionali sono fetch ed execute • Possiamo ulteriormente spezzare la fase di fetch in o Fetch (lettura dell'opcode) o Decode (interpretazione dell'opcode) • Possiamo ulteriormente spezzare la fase di execute in o o o Read (lettura degli operandi) Execute (esecuzione dell'operazione) Writeback (scrittura del risultato) • La struttura così ottenuta si chiama pipeline • Ammettendo che ogni stadio impieghi un ciclo per eseguire il suo compito, la pipeline è in grado di "produrre" un'istruzione al ciclo • Esistono alcune situazioni che rallentano la pipeline: 1. Un'istruzione richiede il risultato della precedente 2. Uno stadio richiede più cicli per eseguire una determinata istruzione 3. Un salto costringe la pipeline a scartare le istruzioni "semilavorate" che non sono più da eseguire • Quando si verificano queste situazioni, nella pipeline si creano bolle (manca l'istruzione da eseguire) o stalli (non si può scaricare l'istruzione nello stadio successivo) • Solo il primo problema può essere risolto in maniera trasparente al programmatore prendendo il dato appena è pronto (dall'uscita della execute) tramite la tecnica del feed forward Tecnologia RISC • Per ottenere il massimo della velocità occorre modificare il set di istruzioni 1. Occorre rendere le istruzioni tutte della stessa velocità, eliminando quelle complesse che richiedono più tempo 2. Occorre modificare l'istruzione di salto, per eliminare le istruzioni scartate • Per il primo punto, si riduce il numero di istruzioni disponibili per il programmatore, lasciando solo quelle semplici, senza indirizzamenti complessi (da qui il nome RISC Reduced Instruction Set Computer) • Per il secondo problema si introduce il delayed jump (salto dilazionato), vale a dire, un salto che comunque esegue l'istruzione che lo segue • Per ridurre gli accessi alla memoria si introduce un numero elevato di registri • La semplificazione di ogni blocco ne consente l'aumento della velocità La seguente tabella3 riassume le differenze più marcate tra i processori CISC e quelli RISC: Numero di istruzioni Modi di indirizzamento Formato delle istruzioni Ciclo medio per istruzione Istruzioni che accedono alla memoria RISC CISC < 100 1-3 1-2 ≅1 solo load/store ≥ 32 HW ≅ 10% > 200 5-20 ≥3 3-10 molte istruzioni Registri 2-16 Unità di controllo microcodice Area destinata alla decodifica delle > 50% istruzioni Naturalmente il confine tra sistemi CISC e RISC non è ben netto se ci si limita solo a considerare il repertorio delle istruzioni, ma, come mostra la tabella, è la presenza di differenze che derivano dal ricorso a metodologie di progetto specifiche che consente di definire un processore RISC o CISC. Architetture Superscalar 3 4 • Per aumentare ulteriormente la velocità si possono mettere in parallelo più pipeline. • Questa architettura prende il nome di superscalar4 • Per alimentare le pipeline occorrerà dimensionare generosamente le cache e aumentare la dimensione del data bus • La tecnica del Feed Forward diventerà molto più complessa e non sarà sempre sufficiente a risolvere il problema http://csc.unisi.it/e2c/dispense/f_in/cap3.pdf Nel Pentium l’architettura superscalare è realizzata con la duplicazione dell’hardware • Il dealyed jump non riesce a risolvere il problema dei salti, in quanto cambia il numero di istruzioni "pendenti". • Aumentando il numero di pipeline: in genere non tutte riescono ad eseguire tutte le istruzioni • Per questo motivo le istruzioni non saranno sempre libere di percorrere qualunque pipeline e dovranno a volte passare da una all'altra • In questo scenario a volte le fasi delle istruzioni o le istruzioni stesse potranno essere eseguite in ordine diverso da come sono nel programma (esecuzione non in ordine) • Per risolvere il problema dei salti si introducono tecniche statistiche di previsione del loro esito, attraverso le jump prediction table Superamento del RISC • Le architetture RISC sono incompatibili con i vecchi processori (68000, 8086) • Se si vogliono far funzionare i nuovi processori con i vecchi set di istruzioni si devono applicare delle tecniche particolari • Gli scogli principali da superare sono: Istruzioni complesse Numero limitato di registri • Le istruzioni complesse sono sempre scomponibili in sequenze di istruzioni semplici • Potremo quindi costruire un processore superscalar in grado di eseguire solo le istruzioni semplici in cui le complesse sono scomposte (che appartengono ad un set RISC) • Potremo creare un "compilatore hardware" che interpreti le istruzioni complesse traducendole in sequenze di istruzioni semplici • Il compilatore hardware alimenterà la cpu superscalar con le istruzioni così ottenute • Non è possibile aumentare i registri che il programmatore conosce (dovremmo modificare il set di istruzioni) • Avendo pochi registri, spesso una relazione di dipendenza deriva dal fatto che serve un registro ma sono tutti occupati (serve una risorsa) • Avendo dei registri in eccesso, potremmo creare una seconda copia del registro indicato (la risorsa), e quindi far partire subito la seconda sequenza di operazioni. • Per fare questo serve una tabella che dica a che registro fisico corrisponde un certo registro logico, per ogni istruzione • Questa tecnica prende il nome di register aliasing Gradi di parallelismo E’ possibile individuare i livelli architetturali nei quali è applicabile il concetto di parallelismo che in ultima istanza deve consentire al sistema un significativo aumento nelle prestazioni. I livelli possono essere distinti a partire dal basso della struttura elettronica e quindi: L0: parallelismo su microistruzioni L1: parallelismo su istruzione L2: parallelismo sul sistema L3: parallelismo sul processo L4: parallelismo interno al programma Evidentemente il parallelismo dei primi tre livelli implica un’evoluzione dell’hardware che non si riflette, almeno in un primo momento, sulla natura del sw che dovrà girarvi sopra. A livello di microistruzioni, a loro volta inserite in microprogrammi associati ad ogni istruzione e utilizzate per espletare una qualsiasi delle fasi di processo di CPU (fetch, decode, operand fetch, execute, operand store), si realizza un grado alto di parallelismo data la scomponibilità di molte operazioni (es. Unità di Floating Point in parallelo, operazioni su Program Counter e di calcolo). La tecnologia RISC (Reduced Instruction Set Computer), implementata parzialmente anche su macchine CISC (Complex Instruction Set Computer, ad es. sullo 80486) contribuisce all’ottimizzazione del codice a livello di microprogramma, riducendone e uniformandone i formati e gli aspetti operativi. A livello di istruzione è evidente come le tecniche di prefetch e di pipeline hanno contribuito a parallelizzare il processo globale in CPU. Le questioni relative a modifiche del program counter ad esempio sono state affrontate predisponendo particolari dispositivi interni alla CPU in grado di predire statisticamente la modifica del Program Counter tramite opportune tabelle storiche dei salti (Branch Table ad esempio adottata sul PENTIUM); in questo caso le code di prefetch possono essere ricostruite in tempo reale e rese disponibili anche in caso di salto. La questione della dipendenza dei dati è stata affrontata predisponendo opportuni buffer interni che consentono la gestione intelligente del data-forwarding, potendo rilevare tempestivamente la dipendenza dei dati tra istruzioni concorrenti e quindi operare salvataggi temporanei che consentono l’avanzamento coerente del processo. Infine la questione dell’accesso concorrente a risorse comuni è stato ampiamente affrontato duplicando opportunamente gli accessi ad esempio al bus di sistema, predisponendo anche in questo caso buffer temporanei di memorizzazione. Il parallelismo di sistema invece fu brillantemente affrontato ad esempio nel caso di trasferimenti pesanti di I/O verso e per la memoria, trasferimenti che potevano essere eseguiti da dispositivi autonomi che si appropriano del bus di sistema consentendo alla CPU di avanzare in parallelo ad esempio in fase di microprogrammazione quando quei bus non sarebbero comunque utilizzati (es. trasferimento diretto in memoria operato dal DMA controller). Quasi tutte le architetture moderne sono coadiuvate da dispositivi autonomi e sincronizzati che sollevano la CPU da gravosi compiti: spesso queste collaborazioni avvengono su fasi parallele (es. interrupt controller quando riconosce l’interruzione, la accoda e la rende con l’id. direttamente alla CPU). Il livello immediatamente superiore invece è assolutamente delegato al sw ed in particolare al sw di sistema operativo. Il parallelismo a livello di processo (task) è governato totalmente da uno specifico SO che ha la capacità di lavorare in multitasking (es. Windows 3.X) o in timesharing (es. UNIX). In realtà siccome condizione necessaria per SO del genere è la predisposizione dell’hw alla protezione della memoria per evitare conflitti tra i diversi task, ci sembra che anche da questo punto di vista l’hw debba essere tenuto doverosamente in considerazione. Il sw comunque garantirà la concorrenza e il parallelismo tramite l’opportuna duplicazione e virtualizzazione delle risorse hw più rilevanti come CPU, Memoria principale e alcune periferiche. Il parallelismo a livello di programma infine costituisce una delle nuove frontiere del mondo informatico: si tratta di strutturare i programmi e gli algoritmi in essi contenuti in modo che la risoluzione sia affrontabile e risolvibile con più CPU. In questo caso si pongono numerose questioni a tutti i livelli e tutte imprescindibili che hanno, di fatto, ostacolato la diffusione di macchine parallele: • strutturazione dell’hardware con n CPU e con una architettura di sincronizzazione specifica (macchine MIMD). • strutturazione del software con metodologie di sviluppo completamente differenziate dalla norma (algoritmi paralleli). • strutturazione del sistema operativo come coordinatore delle due unità hw e sw (SO distribuiti multiprocessor). ARCHITETTURE PARALLELE5 Classificazione di Flynn Nel 1966 Flynn elaborando e sfruttando la sua conoscenza nel mondo dell’informatica descrisse le architetture dei calcolatori suddividendole in quattro classi: SISD: Single Istruction Stream, Single Data Stream. Sono le architetture tradizionali, con una sola unità esecutiva (niente pipeline, niente execution unit multiple) ed una sola memoria. SIMD: Single Instruction Stream, Multiple Data Stream. Ha un’unica unità di controllo e più unità esecutive. Tutte però eseguono la stessa istruzione, ma su dati diversi. Un esempio possono essere i calcolatori vettoriali: eseguono la stessa istruzione (somma, sottrazione, ecc.) ma su vettori di dati (contemporaneamente). Sistemi SIMD attualmente presenti sul mercato sono Connection Machine (della Thinking Machine Co.) e MP (MasPar & DEC). Le applicazioni multimediali e di comunicazione di oggi prevedono spesso loop ripetitivi, che utilizzano il 10% o meno del codice complessivo dell'applicazione impegnando fino al 90% del tempo di esecuzione. La tecnica SIMD consente ad un'istruzione di eseguire la stessa funzione su blocchi multipli di dati, consente inoltre al chip di ridurre i loop ad elaborazione intensiva comuni a video, audio, grafica e animazione. MISD: Multiple Instruction Stream, Single Data Stream. Sono i sistemi basati su pipeline, dove lo stesso dato attraversa diverse unità esecutive che svolgono compiti diversi. Esempi commerciali sono il Cray-1 e il CDC Cyber 205. Vi sono anche alcuni sistemi fault-tollerance, in cui programmi diversi (ma con la stessa funzione) vengono eseguiti sugli stessi dati. MIMD: Multiple Instruction Stream, Multiple Data Stream. Vi sono più unità esecutive, ognuna delle quali ha una propria memoria locale dalla quale legge il programma da eseguire e i dati su cui operare. In alcuni casi i calcolatori vettoriali vengono fatti ricadere in questa categoria, poiché ogni stadio della pipeline può essere considerato come un PE che lavora su dati propri. 5 Da http://multimedia.polito.it/sist-elab-2/aa2001/modulo1/contenuti/materiale-vario/parall_intro.pdf