3 lezione calcolatori

8
Stavamo vedendo i 3 modelli di architettura pensati dal punto di vista di quello che avviene ai dati una volta presi dalla memoria e conservati dentro il processore. Ieri abbiamo visto il modello ad accumulatore e stack. Lo stack ha un vantaggio che consiste in un area di conservazione maggiore rispetto all’aria monocratica del processore ad accumulatore. Ha, insieme a quello precedente, il vantaggio di avere un indirizzamento implicito del dato. Difatti mentre nel 1° non potevo dare nessun tipo di specifica al dato perché sottinteso ( mettilo direttamente nell’ accumulatore) anche nel 2° caso non devo specificare nulla: Pop x, push x ( è chiaro che il dato va messo nello stack). Questo significa avere delle istruzioni più compatte perché le istruzioni di tipo aritmetico-logiche sono implementate nello stesso modo con cui sono implementate nella macchina ad accumulatore, anzi ancora meglio se vogliamo perché nell’accumulatore dicevo una somma e avevo un indirizzo nel senso che facevo Accumulatore + y e il risultato andava in accumulatore. Nella macchina a stack dico solamente ADD nel senso che il codice intende: prendi il dato sullo stack, dopo di che ,sullo stack, dopo che ho preso quel dato c’è ne è un altro. Prendilo, fai la somma e il risultato lo rimetti nello stack. L’ultimo dato del mio stack sarà quindi il risultato trovato precedentemente. In pratica ho una struttura efficiente usabile per il calcolo delle espressioni matematiche. Per esempio se abbiamo una espressione del tipo : (z*(y+3x-5)/q) ( risolviamo i calcoli con maggiore precedenza). Per cui questa espressione se la vado a scrivere correttamente in uno stack funziona bene. Il sistema per funzionare bene tira fuori la x poi il * ( posso implicitamente caricare anche le operazioni nella pila) e poi tiro fuori il 3 valore che scrivo successivamente per continuare le operazioni. Sostanzialmente le calcolatrici scientifiche ,quelle che gestiscono parantesi e altro, funziona proprio con questo meccanismo, il loro processore gestisce i dati con modalità stack. In più gestendo le parentesi va a scrivere correttamente nello stack. Andiamo ora avanti, capiamo che il limite di questa architettura è che i dati, anche se ne ho più di uno, non vi posso accedere maniera casuale ( Che significa? E’ diverso dal significato nel linguaggio casuale cioè non prendere un dato qualsiasi, ma nell’informatica significa che io posso accedere con una certa volontà in maniera del tutto indipendente dagli accessi che ho fatto in precedenza. Ho cioè la libertà di prendere quel determinato dato senza nessun collegamento con quello che ho fatto

description

Appunti presi a lezione e poi trascritti al pc per avere miglior leggibilità

Transcript of 3 lezione calcolatori

Page 1: 3 lezione calcolatori

Stavamo vedendo i 3 modelli di architettura pensati dal punto di vista di quello che avviene ai dati una volta presi dalla memoria e conservati dentro il processore. Ieri abbiamo visto il modello ad accumulatore e stack. Lo stack ha un vantaggio che consiste in un area di conservazione maggiore rispetto all’aria monocratica del processore ad accumulatore. Ha, insieme a quello precedente, il vantaggio di avere un indirizzamento implicito del dato. Difatti mentre nel 1° non potevo dare nessun tipo di specifica al dato perché sottinteso ( mettilo direttamente nell’ accumulatore) anche nel 2° caso non devo specificare nulla: Pop x, push x ( è chiaro che il dato va messo nello stack). Questo significa avere delle istruzioni più compatte perché le istruzioni di tipo aritmetico-logiche sono implementate nello stesso modo con cui sono implementate nella macchina ad accumulatore, anzi ancora meglio se vogliamo perché nell’accumulatore dicevo una somma e avevo un indirizzo nel senso che facevo Accumulatore + y e il risultato andava in accumulatore. Nella macchina a stack dico solamente ADD nel senso che il codice intende: prendi il dato sullo stack, dopo di che ,sullo stack, dopo che ho preso quel dato c’è ne è un altro. Prendilo, fai la somma e il risultato lo rimetti nello stack. L’ultimo dato del mio stack sarà quindi il risultato trovato precedentemente. In pratica ho una struttura efficiente usabile per il calcolo delle espressioni matematiche. Per esempio se abbiamo una espressione del tipo : (z*(y+3x-5)/q) ( risolviamo i calcoli con maggiore precedenza).Per cui questa espressione se la vado a scrivere correttamente in uno stack funziona bene. Il sistema per funzionare bene tira fuori la x poi il * ( posso implicitamente caricare anche le operazioni nella pila) e poi tiro fuori il 3 valore che scrivo successivamente per continuare le operazioni. Sostanzialmente le calcolatrici scientifiche ,quelle che gestiscono parantesi e altro, funziona proprio con questo meccanismo, il loro processore gestisce i dati con modalità stack. In più gestendo le parentesi va a scrivere correttamente nello stack.Andiamo ora avanti, capiamo che il limite di questa architettura è che i dati, anche se ne ho più di uno, non vi posso accedere maniera casuale ( Che significa? E’ diverso dal significato nel linguaggio casuale cioè non prendere un dato qualsiasi, ma nell’informatica significa che io posso accedere con una certa volontà in maniera del tutto indipendente dagli accessi che ho fatto in precedenza. Ho cioè la libertà di prendere quel determinato dato senza nessun collegamento con quello che ho fatto prima. Non come avveniva nello stack dove sono vincolato a prendere il precedente o il successivo-> dipende dal SP). Avere più dati all’interno del processore è sicuramente una cosa positiva ma sarebbe ancora meglio se tutti fossero accessibili in maniera casuale. Per fare questo devo pensare ad un meccanismo più complesso, avere una serie di celle ciascuna che può contenere un dato e avere la libertà di accedere ad una determinata di queste senza seguire un qualche ordine. Per fare ciò devo poter gestire un accesso a questa struttura ma devo anche (effetto collaterale) pensare a specificare questi dati. Mentre prima dicevo POP e non avevo bisogno di specificare nient’altro, qui non posso dire leggi e basta ma devo dire leggi e cosa andare a leggere. Quindi una struttura di questo genere devo specificare necessariamente gli operandi, e se una istruzione prevede 3 operandi, 2 sorgenti e uno di arrivo(destinazione) dovrò necessariamente specificarli tutti e 3.(nello stack non specificavo niente- nell’accumulatore solo uno).facendo si, di conseguenza, che le istruzioni diventino più lunghe ( immagina una macchina stack che fa 4 operazioni, basta 1 solo codice operativo di 2 bit, in realtà sarà più lunga perché prevede anche gli indirizzi, però posso anche pensare che gli indirizzi sono nello stack). La macchina che abbiamo appena descritto è detta a registri ( le locazioni di prima sono i registri) e quindi avremo un sistema che sarà dimensionato dal numero di registri e dalla lunghezza di parola ( processore a 32 registri per esempio). In questa prima parte del corso penseremo a processori che lavorano solo su dati interi ( dopo vedremo cosa aggiungere al processore per poter lavorare anche con dati floating point).

Page 2: 3 lezione calcolatori

Una macchina a registri deve prevedere una serie di strutture che complicano anche l’hardware per: gestire l’accesso ai registri per esempio o un controllo che dica quale dei registri deve andare in ingresso all’ALU quando deve fare una operazione. Quindi ritornando al discorso fatto l’altro giorno una macchina a registri diventa LOAD/STORE nel momento in cui quella macchina fa si che le operazioni interessano soltanto i dati che sono dentro i registri o al più dati che sono dentro i registri con l’eccezione che un operando sorgente possa anche essere un operando scritto nell’istruzione stessa. Ho tre possibili locazioni di dove possono stare gli operandi: o in memoria o nei registri o nell’istruzione stessa (ovviamente non il risultato perché non ha senso farglielo ricalcolare). Dal momento in cui il compilatore si accorge che nel codice stesso vi è il valore dell’operando può evitare di prendere quel valore e scriverlo nel registro. ( Esempio del for) Quando scriverò una istruzione al posto di scrivere una somma di un certo registro r1 e un certo registro r2 e metto il risultato in r1 posso dirgli direttamente di fare r1+1 senza sprecare memoria in r2 e questo 1 lo metto direttamente nell’istruzione. Non è più una istruzione ADD ma ADDI(altro codice operativo). Qual è la differenza? L’ALU deve fare sempre un + però quando ho una istruzione non bisogna vederla ridotta ad una sola operazione, ma va vista nel suo complesso poiché prima di arrivare a fare quel + il processore esegue altre operazioni. Nell’ADDI uno dei dati in ingresso all’ALU viene dato direttamente nell’IR ed è il numero 1, nella ADD per prendere 2 dati in ingresso li devo entrambi prendere dai registri. Queste due operazioni sono diverse anche se l’ALU fa sempre una somma. In più non devo pensare che per eseguire una istruzione non devo sempre pensare di usare l’ALU dato che alle volte non viene neanche tirata in ballo. Per fare un’istruzione devo eseguire dei passi che chiameremo micro-istruzioni. Una istruzione è composta da una sequenza di microistruzioni.Quindi io posso immaginare una istruzione in cui ho un immediato scritto nell’istruzione stessa ( come nel LOAD/STORE dove l’immediato era un pezzo dell’indirizzo).Un altro modo di classificare i processori è quello che sancisce il fatto che quel processore abbia un set di istruzioni a lunghezza fissa o variabile. Cosa significa? Per eseguire un programma l’unità di calcolo esegue una sequenza di istruzioni che va a pescare dalla memoria ( dove stanno scritte le istruzioni caricate dal loader che è quella parte di codice operativo che carica il programma in memoria altrimenti il processore non può leggerlo dato che non può leggere direttamente dall’hard disk). Inoltre per leggere le istruzioni il processore deve sapere quanti byte deve leggere ( posso avere un’istruzione da tot byte). In realtà oggi esiste un solo approccio anche se di base sono 2 dato che in passato si è discusso a lungo su questo determinato argomento.

1. Tutte le istruzioni hanno lunghezza costante ( le istruzioni sono tutte di 32 bit per esempio)

2. Un altro approccio dice di non vincolare tutte le istruzioni ad essere della stessa lunghezza. Questo secondo approccio aveva un certo senso quando si pensava ai processori di tipo CISC. Questo perché quel processore può essere tale da specificare un codice operativo, 2 operandi sorgenti tramite due indirizzi in memoria e quello destinazione con un indirizzo in memoria. Questa istruzione ( oltre codice operativo anche 3 indirizzi) richiede un certo numero di bit per avere un senso ( pochi non conviene perché significa che indirizziamo una memoria molto piccola) e quindi l’istruzione ha una certa lunghezza. Se ho per esempio la memoria di 1MB l’indirizzo è di 20 bit quindi ho 60 bit per mettere questi 3 indirizzi. Se per esempio un indirizzo lo devo individuare prendendo questo immediato e sommandolo al contenuto di un registro, dovrò anche dire qual è il registro dove prendo la base. Nel processore ho 32 registri le parole sono di 5 bit( 32= 2^5). Quando specifico un registro devo dare il nome di quel registro e devo dire quanto è lungo quel “nome”. In questo caso abbiamo

Page 3: 3 lezione calcolatori

5 bit. Siamo arrivati a 78 bit più un certo numero per codificare il codice operativo. Quindi in una macchina CISC le istruzioni possono avere una certa lunghezza. Se io ho quindi questa macchina CISC con lunghezza costante l’istruzione farò in modo tale da prendere la più lunga in modo da vincolare tutte le altre ad avere la stessa lunghezza: c’è poi una istruzione che mi dice che ho finito il programma. Questa istruzione prevede solo il codice operativo, non ho bisogno di dire altro. Siccome ho scelto di fare lunghezza costante anche questa avrà 96 bit. Fondamentalmente quindi sto sprecando memoria.

Vantaggio lunghezza fissa: tutta la gestione del prelievo dell’ istruzione è semplificato. Tutte le istruzioni hanno quella lunghezza ( 32 bit per esempio) li carico e so che mi sono caricato l’istruzione. Lo svantaggio è che non posso avere delle istruzioni più lunghe oppure può sprecando memoria ( se per esempio metto 1 solo bit su 32)Vantaggi lunghezza variabile: la lunghezza si adatta in base all’istruzione stessa. Chiaramente devo dare pur sempre dei “tagli” tipo 32-64-96 e poi le istruzioni che riescono a stare in 32 vanno in 32, quelle da 64 in 64 e cosi via. Ovviamente se ho una da 90 andrà in 96 ma sprecherò comunque solo 6 bit. Una macchina di questo genere può funzionare con 2 possibilità : essendo la lunghezza variabile pongo in un colpo 96 bit di istruzione. Se ho una memoria con una banda così larga ( banda= la quantità di dati che posso leggere o scrivere in una memoria in una unità di tempo). Se riesco in un colpo di clock a leggere 96 bit va bene. Razionalmente andrebbe meglio impostare 32 bit di memoria e se mi serve di più leggo altri pezzi. Come faccio a sapere se dopo aver letto 32 bit ho letto una istruzione intera d 32 bit o la prima di 2 puntate o la prima di 3 puntate. (Potrebbe finire con 00 per farci capire che è finita l’istruzione ma sto comunque sprecando 2 bit in più potrebbe essere che un determinato dato finisca già con 00 e quindi ci sarebbe una incomprensione).Fortunatamente il codice operativo dell’istruzione è comunque nella prima puntata, se ho prelevato la prima parola ho comunque questo codice più una serie di informazioni ( operandi, indirizzi ecc) e se è lunga o corta è implicita nel codice operativo. So quindi che istruzione ho e quanto è lunga e che magari devo andare a prendermi altri pezzi dalla memoria. E’ cosi che gestisco l’uso di un processore che lavora con formato di parole a lunghezza variabile, non sto sprecando molto bit . Per cui se ho un’ ampia dinamica di istruzioni può essere più utile una gestione a lunghezza variabile, se ho una dinamica abbastanza simile (istruzioni più o meno tutte lo stesso formato) è meglio la lunghezza costante. Va da sé che le macchine CISC con un set complesso sono meglio implementante con lunghezza variabile, macchine RISC con istruzioni abbastanza simili come formato non hanno senso con logica a lunghezza variabile ma meglio lunghezza costante. Bisogna tenere presente che stiamo dimensionando un sistema e quando compio questa operazione devo tenere conto di tutti quanti i parametri del sistema . Bisogna fare un bilancio con tutti quanti i componenti.Iniziamo ora una parte un po’ più dettagliata: considero un processore RISC perché abbiamo visto che se pur può sembrare un limite per fare un qualcosa necessita di più istruzioni vedremo che il fatto che richiede queste istruzioni ( codice più lungo ovviamente) anche se sono più compatte. Nel cominciare a pensare a questo processore partiamo dal suo formato istruzioni: è un processore RISC con set di istruzioni a lunghezza costante, definisco che la lunghezza costante di queste istruzioni è 32 bit cioè 4 byte e definisco anche ( dato che questo numero non è un problema a livello tecnologico) 32 è la dimensione della parola e la banda di memoria del sistema stesso. Quindi le connessioni all’interno del processore e tra processore e altri elementi è un bus a 32 bit. Cosa è un bus? Questo deriva dalla parola latina omnibus ed è un canale di comunicazione tra due

Page 4: 3 lezione calcolatori

elementi. Supponiamo di avere due oggetti che possono essere connessi che comunicano con un canale riservato. Invece quello che chiamiamo bus non è dedicato a far parlare solo due elementi ma è usato per far parlare tra di loro tanti elementi. E’ chiaro che ci sono vantaggi e svantaggi, possono “parlare” in tanti ma in realtà non contemporaneamente. Posso quindi parlare con tutti dato che tutti potranno “ascoltarmi” ma solo uno potrà rispondermi. Ovviamente più sono gli elementi collegati al bus più esso è inefficiente. Quando scrivo devo dire a chi sto scrivendo, tutti gli altri elementi possono leggere ma solo il diretto interessato potrà “rispondere”. Se sono stato prudente ho dato delle altre porte libere in modo in un futuro di poter aggiungere un collegamento, ma anche il numero di porte libere è limitato. Con un bus puoi gestire in maniera adeguata un certo numero di utenti in base al traffico richiesto da questi utenti.Una volta determinata la parola abbiamo una serie di ridimensionamenti. Questo processore per ora tratti dati interi, questi dati esistono in 4 formati:- Dato word: 32 bit- Dato long: 64 bit- Dato half: 16 bit- Dato byte: 8 bitQuesti dati vengono gestiti con e senza segno. Per quanto riguarda la long sembra esserci un controsenso dato che abbiamo stabilito come lunghezza 32, difatti il dato sarà considerato in una coppia di registri da 32 bit, cioè siccome questo dato non sarà frequente quando avrò bisogno di usare dati da 64 bit li userò a puntate accoppiando registri da 32 bit. Viceversa i registri da 32 bit sono utilizzati per contenere dati da un byte o da 2 byte sprecando quindi della memoria. Il numero di registri del nostro processore è 32 bit e ciò significa che noi potremo specificare uno di questi 32 registri con 5 bit.La lunghezza delle istruzioni di questo processore è costante che però non significa che le istruzioni abbiano tutte la “stessa faccia” perché nell’ambito della lunghezza costante si avranno 3 diversi formati di istruzioni: specificheremo col formato istruzione 3 modalità diverse di usare i 32 bit con cui l’istruzione stessa è codificata:-I ( Immediato)-J ( Jump)-R (Registro)Questi formati sono fatti nel seguente modo:*Nelle istruzioni di tipo J occorre specificare un operando che è puramente immediato. Per questo avendo solo la necessità di specificare un immediato tolti i 6 di codice operativo tutti i 26 li destineremo all’immediato.*Nel caso di I ho 16 bit per l’immediato mentre ho due da 5 bit per i registri. Sono istruzioni che fanno uso di aritmetica in cui un operando è immediato (16)*Nel caso di R ho un registro in più. Gli altri ultimi 11 bit che avanzano sono bit che servono a specificare meglio l’operazione codificata nel codice operativo e prenderanno il nome di campo funzione. (possiamo anche far finta che questi 11 bit non ci siano)Notiamo che J sono i salti incondizionati, noi in programmazione nel codice che scriviamo usiamo salti condizionati del tipo se a>b fai questo altrimenti quest’altro. Dal punto di vista del flusso “fai questo al posto di un altro” si codifica in una serie di operazioni dove nell’ultima parte ho un salto ad una altra parte del programma ( si deve verificare una condizione affinché ciò sia possibile). Questo tipo di salto in letteratura prende il nome di brench. Viceversa il Jump che è il salto incondizionato, è quando eseguo per l’appunto un salto senza pormi delle domande (ovvero avere delle condizioni). Un esempio di salto

Page 5: 3 lezione calcolatori

incondizionato è quando sono entrato in un codice che esegue una routine, termina la routine e mi pongo il problema di dove devo tornare.Ovviamente devo tornare da dove provenivo e questo salto, come già detto, lo devo fare a prescindere. ( il salto codifica un indirizzo da dove tornare)Prima di andare oltre vediamo di capire come è gestito il ritorno da subroutine. Prima però la differenza tra funzione e procedura. Questi sono dei moduli del mio algoritmo che io chiamo in questa maniera perché trattano una serie di operazioni ripetitive. Se mi accorgo che c’è in più punti del mio programma qualcosa che si ripete decido di utilizzare la chiamata a quelle istruzioni ripetute. La differenza filosofica che esiste tra quei due termini è che mentre la funzione restituisce qualcosa, la procedura non necessariamente restituisce qualcosa. Entrambe possono modificare dei valori delle variabili globali però se ho un blocco di istruzioni che produce un risultato e lo restituisco non posso chiamarla procedura ma funzione. Tra gli effetti collaterali della chiamata con ritorno di un qualcosa vi è la creazione di un numero che non è sempre solo un numero ma può anche essere un vettore.Nel linguaggio macchina sia procedure che funzioni prendono il nome di subroutine, e sono alcune linee di codici codificate a parte che però nel mio programma riesco a chiamare grazie ad una serie di istruzioni apposite. Detto ciò dobbiamo avere un meccanismo per gestire l’uso di queste subroutine ( hanno senso solo se chiamate in più punti del programma) e soprattutto devo capire dove ritornare. Devo inoltre gestire un’istruzione che mi fa tornare da dove avevo chiamato che non può essere un semplice Jump all’indirizzo k perché in questo modo la subroutine tornerà sempre all’indirizzo k. Per questa motivazione per gestire una situazione del genere esiste una istruzione che è specifica per questo. ( ovvero che mi permette di tornare sia all’indirizzo k+1 se chiamato da indirizzo k o j+1 se chiamato da indirizzo j per esempio). Anche per le funzioni ricorsive si ha il discorso appena fatto, vengono cioè riconosciute come subroutine e bisogna vedere come gestire il loro ritorno.Provare a pensare come io riesco a gestire il ritorno di subroutine per venerdì.