Vai al contenuto

Lezione 07 · Il modello di von Neumann

Cosa impari

  • quali sono le unità del modello di von Neumann: CPU, memoria, input, output, bus;
  • come la CPU esegue un programma con il ciclo fetch – decode – execute e a cosa servono i suoi registri;
  • come funziona la memoria (indirizzi, parole, LOAD e STORE) e come si classificano le memorie;
  • il ruolo dei bus e del clock, e come calcolare spazio di indirizzamento e periodo del clock;
  • le evoluzioni del modello: canali, interruzioni, cache, gerarchia di memoria, software.

Quando esegui un programma Python, dietro le quinte c'è una macchina che preleva istruzioni dalla memoria e le esegue una alla volta, miliardi di volte al secondo. Lo schema di questa macchina è quasi lo stesso dal 1945: il modello di von Neumann. Capirlo ti aiuta a sapere che cosa succede davvero quando il tuo codice gira.

Il calcolatore

Un calcolatore è una macchina che esegue in modo automatico, ad altissima velocità, operazioni elementari dettate da un algoritmo memorizzato.

  • Non ha nessuna capacità decisionale: compie solo le azioni previste dalla procedura.
  • Esegue milioni di istruzioni al secondo (si misura in MIPS).
  • Sa fare solo un numero limitato di operazioni.
  • L'algoritmo gli va comunicato in un linguaggio che capisce.

Un po' di storia

  • Prime macchine non programmabili, meccaniche, di supporto al calcolo: gli studi di Leonardo da Vinci (circa 1500) e la Pascalina di Blaise Pascal (1643).
  • Prima macchina programmabile: la macchina analitica di Charles Babbage (1834).
  • L'intuizione di Alan Turing: far eseguire all'hardware istruzioni codificate in un programma (software) che si può inserire e modificare dall'esterno.
  • L'architettura proposta da John von Neumann realizza questa idea: nasce il computer programmabile in senso moderno.
  • Dall'ENIAC (macchina limitata, quasi senza memoria, poco flessibile) all'EDVAC, fino ai personal computer e agli smartphone di oggi.

Il modello di von Neumann

È lo schema di principio dei computer tradizionali. Fu proposto da von Neumann nel 1945.

flowchart LR
    IN["Input"] --> CPU["CPU"]
    CPU --> OUT["Output"]
    CPU <--> MEM["Memoria"]
Unità Compito
CPU (Central Processing Unit) coordina ed esegue le operazioni fondamentali
Memoria contiene l'algoritmo (le istruzioni) e i dati su cui lavora
Unità di input permette di inserire algoritmo e dati in memoria
Unità di output presenta i risultati del lavoro della CPU

Caratteristiche del modello

  • Programma memorizzato: la macchina tiene nella stessa memoria sia i dati sia le istruzioni. È l'idea chiave del modello.
  • Flessibilità: macchine nate per fare calcoli risolvono anche problemi amministrativi, gestionali, produttivi. Basta cambiare programma.
  • Velocità: milioni di istruzioni al secondo.
  • Affidabilità: il computer non sbaglia mai l'esecuzione dell'algoritmo, perché lo esegue in modo obbediente. Se il risultato è sbagliato, l'errore è nell'algoritmo.
  • Capacità di memoria adeguata, misurata in byte.
  • Costo vantaggioso.

La CPU

La CPU (detta anche processore) acquisisce, interpreta ed esegue il programma che si trova nella memoria centrale, trasformando i dati. Ha tre parti:

  • Unità di Controllo (CU, Control Unit);
  • Unità Logico-Aritmetica (ALU, Arithmetic Logic Unit);
  • registri interni.

L'Unità di Controllo e il ciclo del processore

La CU interpreta le istruzioni una alla volta e attiva tutti i meccanismi necessari per eseguirle. Lavora in modo ciclico, ripetendo sempre le stesse fasi: il ciclo del processore.

flowchart TD
    B["Boot"] --> F["Fetch: preleva l'istruzione"]
    F --> D["Decode e Operand Assembly: interpreta l'istruzione e preleva i dati"]
    D --> E["Execute: esegue e salva il risultato"]
    E --> F
Fase Che cosa succede
Boot operazione iniziale: comunica alla CU l'indirizzo di memoria della prima istruzione da eseguire
Fetch l'istruzione viene prelevata dalla memoria centrale e copiata in un registro interno (IR)
Decode & Operand Assembly si interpreta il codice operativo dell'istruzione e si prelevano dalla memoria i dati che servono
Execute l'istruzione viene eseguita e il risultato, se c'è, viene salvato

Che cosa vuol dire "eseguire" un'istruzione? La CU invia una sequenza di segnali di abilitazione ai vari dispositivi. Ogni istruzione corrisponde a una sequenza di microistruzioni, conservate in una memoria interna alla CU. L'insieme di questi microprogrammi si chiama firmware.

L'ALU

L'ALU esegue le operazioni aritmetiche, di confronto e bit a bit (AND, OR, NOT, viste nella lezione 06). L'esito dei calcoli (ad esempio "risultato zero" oppure "overflow") viene segnalato da appositi bit nel registro Condition Code (CC). Nei sistemi moderni l'ALU è affiancata da un processore matematico (FPU) per i numeri in virgola mobile.

I registri interni

I registri sono piccole memorie dentro la CPU. Sono molto più veloci della memoria centrale. I principali:

Registro A cosa serve
IR (Instruction Register) contiene l'istruzione che la CU sta eseguendo
PC (Program Counter, o Prossima Istruzione) contiene l'indirizzo della prossima istruzione; dopo ogni fetch viene aggiornato
ACC (Accumulatore) deposito di dati per l'ALU: prima contiene un operando, alla fine il risultato
CC (Condition Code) o SR (Status Register) segnala le condizioni dell'elaborazione: risultato nullo, overflow, NaN…

Nello schema interno della CPU compaiono anche MAR (Memory Address Register, l'indirizzo da leggere o scrivere in memoria) e MDR (Memory Data Register, il dato letto o da scrivere).

La memoria

La memoria è un insieme di registri (contenitori) di dimensione fissa. Ogni registro ha una posizione, detta indirizzo.

  • La memoria centrale è organizzata come una sequenza di parole (word) di m bit: m è la lunghezza di parola (8, 16, 32, …). La CPU legge o scrive gli m bit di una parola con un'unica operazione.
  • Ogni parola ha un indirizzo, un intero tra 0 e 2ᴺ − 1, dove N è il numero di bit usati per gli indirizzi. Il numero di indirizzi, 2ᴺ, si chiama spazio di indirizzamento.

Puoi immaginare la memoria come una tabella a due colonne:

Indirizzo Dato (m bit)
0 0010101000101010
1 0101101001011010
2 0010101011111001
… …
2ᴺ − 1 1010101010101010

Lettura e scrittura

Operazione Nome Effetto
Lettura LOAD preleva il contenuto del registro senza distruggerlo
Scrittura STORE inserisce un dato nel registro cancellando quello precedente

Il buffer è l'area di transito dei dati tra CPU e memoria. Il funzionamento è questo:

  1. la CPU indica l'indirizzo del registro;
  2. la memoria decodifica l'indirizzo e abilita solo quel registro;
  3. con uno STORE il dato passa dal buffer al registro; con un LOAD dal registro al buffer.

LOAD e STORE in Python

Quando scrivi x = y + 1, il processore fa (semplificando) un LOAD del valore di y, la somma nell'ALU e uno STORE del risultato nella cella di x. Il valore di y resta intatto; quello vecchio di x viene sovrascritto.

Prestazioni e tipi di memoria

Le prestazioni di una memoria si misurano con il tempo di accesso: il tempo che passa tra la selezione del registro e la disponibilità del dato (LOAD) o il suo deposito (STORE). Le memorie veloci costano di più: bisogna trovare un compromesso tra prestazioni e costi.

Criterio Tipo Caratteristica
modo di accesso RAM (Random Access Memory) accesso casuale: il tempo non dipende dalla posizione
SAM (Sequential Access Memory) accesso sequenziale: il tempo dipende dalla posizione (come un nastro)
durata dei dati volatile perde i dati quando si spegne il sistema (RAM e memorie elettroniche)
permanente conserva i dati da spento (memorie magnetiche, ottiche, ROM)

Una ROM (Read Only Memory) si scrive una sola volta e poi si può solo leggere. Esempio: il BIOS (Basic Input-Output System), i programmi di base che permettono al sistema operativo di accedere all'hardware della scheda madre.

Memorie di massa e buffer

Le memorie di massa (dischi, SSD, chiavette) sono memorie ausiliarie di grande capacità ma più lente della memoria centrale. Il loro uso segue sempre lo stesso percorso:

  • lettura: memoria di massa → memoria centrale → CPU;
  • scrittura: CPU → memoria centrale → memoria di massa.

La CPU non lavora mai direttamente sulla memoria di massa: i dati passano sempre dalla memoria centrale.

Per conciliare dispositivi a velocità diverse, la memoria centrale contiene dei buffer:

  • buffer di input: accumula i dati che arrivano da un dispositivo lento, prima che la CPU li elabori;
  • buffer di output: la CPU, molto più veloce, vi deposita i dati prodotti prima di trasferirli.
flowchart LR
    IN["Input"] --> BI["Buffer di input"]
    BI --> MEM["Memoria centrale"]
    MEM <--> CPU["CPU"]
    MEM <--> MM["Memorie di massa"]
    MEM --> BO["Buffer di output"]
    BO --> OUT["Output"]

Input e output

  • Unità di input: tastiera, mouse, penna ottica, tavoletta grafica, scanner, …
  • Unità di output: monitor, plotter, stampanti, …

I dati da tastiera e verso lo schermo sono flussi di caratteri (codificati in ASCII, lezione 05). Servono quindi delle conversioni di formato: da caratteri a binario in ingresso, da binario a caratteri in uscita. È quello che fanno int(input()) e print() in Python.

Il bus

Per scambiare informazioni tra le unità ci sono due possibilità: collegare ogni componente con tutti gli altri, oppure collegarli tutti a un unico canale, il bus. Il bus rende il calcolatore modulare ed espandibile.

Il bus è condiviso, ma collega due unità alla volta (una trasmette, l'altra riceve), sotto il controllo della CPU. Ne esistono tre tipi:

Bus Che cosa trasporta
Control Bus i comandi della CU, ad esempio i segnali read (lettura) e write (scrittura)
Address Bus l'indirizzo del registro di memoria o del dispositivo interessato
Data Bus i dati da trasferire

Come avviene un trasferimento. L'unità che inizia (di solito la CPU) mette l'indirizzo sull'Address Bus e il comando sul Control Bus (ad esempio READ). Tutti i componenti "ascoltano" l'Address Bus: si attiva solo quello che riconosce il proprio indirizzo. Il dato viaggia poi sul Data Bus.

  • Le memorie prelevano o immettono dati sul Data Bus, a seconda del comando.
  • I dispositivi di input possono solo immettere dati sul Data Bus.
  • I dispositivi di output possono solo prelevare dati dal Data Bus.

Larghezza del bus

  • Bus seriale: un solo filo, i bit passano uno dopo l'altro.
  • Bus parallelo: n fili, passano n bit alla volta (8, 32, …). Address Bus e Data Bus sono paralleli.

Le due formule da ricordare

  • Con un Address Bus a n bit si possono selezionare 2ⁿ registri diversi: è la capacità di indirizzamento.
  • Con un Data Bus a m fili viaggiano m bit contemporaneamente: più è largo, più è veloce lo scambio.
n = int(input('Bit dell\'Address Bus: '))
indirizzi = 2 ** n
print('Registri indirizzabili:', indirizzi)
print('Indirizzi da 0 a', indirizzi - 1)
# Se ogni registro contiene 1 byte:
print('Memoria massima:', indirizzi / 1024, 'KB =', indirizzi / 1024 / 1024, 'MB')

Prova con 16, 20 e 32: con 16 bit ottieni 64 KB, con 20 bit 1 MB, con 32 bit 4096 MB = 4 GB (se ogni indirizzo corrisponde a un byte).

Il clock

La CU coordina tutti i componenti secondo sequenze prestabilite. Per sincronizzarli serve un orologio interno che scandisce il ritmo: il clock.

Il clock è un segnale periodico, un'onda quadra con:

  • periodo T (detto ciclo), la durata di un battito;
  • frequenza f = 1 / T, i battiti al secondo, misurata in hertz (Hz).
Unità Battiti al secondo
1 Hz 1
1 MHz 1 milione
1 GHz 1 miliardo

Esempio: un clock con 10 cicli al secondo ha f = 10 Hz e T = 1/10 s = 100 ms.

Clock e prestazioni. Se ogni ciclo di clock esegue esattamente un'operazione, la frequenza indica le operazioni al secondo: un processore a 3 GHz esegue 3 miliardi di operazioni al secondo. In realtà un'operazione può richiedere più cicli, perché è complessa o perché i dispositivi collegati (come la memoria centrale) sono più lenti della CPU. Per questo i bus lavorano spesso a una frequenza più bassa, anche di un fattore 10.

f_ghz = float(input('Frequenza del clock in GHz: '))
f = f_ghz * 1_000_000_000      # in Hz
T = 1 / f                      # in secondi
print('Periodo T =', T, 's')
print('Periodo T =', T * 1_000_000_000, 'ns')
print('Operazioni al secondo (1 per ciclo):', f)

L'evoluzione del modello

Le istruzioni macchina

La CPU interpreta un linguaggio fissato, il linguaggio macchina, formato da un insieme di istruzioni detto repertorio (instruction set), diverso per ogni CPU. Un'istruzione macchina è una quadrupla:

i = (Cop, Pdi, Pdo, Pis)

  • Cop: codice operativo, dice alla CU quale operazione fare;
  • Pdi: puntatori ai dati di input dell'operazione;
  • Pdo: puntatori ai dati di output prodotti;
  • Pis: puntatore alla prossima istruzione da eseguire.

Le istruzioni del repertorio sono semplici: trasferire dati tra registri, operazioni aritmetiche e logiche dell'ALU (somma, AND, OR, NOT, rotazione dei bit), controllo delle condizioni nel registro CC.

Logica cablata e microprogrammata

Per eseguire un'operazione la CU invia una sequenza di abilitazioni. Ci sono due modi di realizzarla:

  • logica cablata: la sequenza è realizzata direttamente da circuiti elettronici collegati in un certo modo. È veloce ma poco flessibile: per cambiarla bisogna cambiare il circuito. Le prime CPU erano così;
  • logica microprogrammata: ogni istruzione è un piccolo microprogramma, una sequenza di microistruzioni semplici lette da una memoria interna alla CU. È molto flessibile.

CISC e RISC

Le CPU CISC (Complex Instruction Set Computer) hanno istruzioni potenti e complesse: ad esempio leggono un dato in memoria, lo modificano e lo salvano con una sola istruzione. Storicamente sono state realizzate soprattutto con logica microprogrammata. Le CPU RISC (Reduced Instruction Set Computer) hanno poche istruzioni semplici, adatte a una logica cablata veloce.

Canali e interruzioni

Nel modello originale la CPU gestiva tutto da sola: non si potevano sovrapporre input e output. Sono nati i canali: processori dedicati (per input/output, grafica, numeri reali, …) che lavorano in autonomia, anche in parallelo alla CPU.

Quando un canale ha finito il suo compito, avanza una richiesta di interruzione (interrupt) per avere l'attenzione della CPU. Al termine di ogni istruzione la CU controlla un bit del registro CC:

  • CC = 0: nessuna interruzione, preleva l'istruzione successiva;
  • CC = 1: esegue un programma del sistema operativo, l'ISR (Interrupt Service Routine). L'ISR capisce quale dispositivo ha chiesto l'interruzione e, se le richieste sono più di una, sceglie quale servire per prima in base alla priorità.

La memoria cache

La cache è una memoria molto veloce posta tra la memoria centrale e la CPU. Istruzioni e dati vengono copiati in anticipo dalla memoria centrale nella cache; la CU li preleva dalla cache. Quando ciò che serve non c'è, scatta un nuovo trasferimento dalla memoria centrale.

  • Cache di primo livello (L1): interna alla CPU, la più veloce.
  • Cache di secondo livello (L2): più lenta di L1 (4–5 volte), ma più veloce della RAM (che è 20–30 volte più lenta di L1).

La cache funziona grazie ai principi di località:

  • località temporale: un programma tende a riusare dati e istruzioni usati di recente (pensa alle istruzioni di un ciclo for);
  • località spaziale: un programma tende a usare dati vicini in memoria a quelli appena usati (pensa agli elementi consecutivi di una lista).

La gerarchia di memoria

La gerarchia dà ai programmi l'illusione di una memoria grande e veloce. I livelli vicini alla CPU sono veloci ma piccoli (e costosi); quelli lontani sono grandi ma lenti. Ogni livello fa da buffer al successivo.

flowchart LR
    CPU["CPU e registri"] --- L1["Cache L1"] --- L2["Cache L2"] --- RAM["Memoria centrale"] --- MM["Memoria di massa"]

Da sinistra a destra: velocità e costo diminuiscono, capacità aumenta.

Allocazione in memoria

Allocare significa associare istruzioni e dati ai registri di memoria.

  • Memorie a voce: un solo registro per ogni istruzione o dato. Memorie a byte: un dato può occupare più registri.
  • Allocazione statica (prima dell'esecuzione) o dinamica (durante l'esecuzione).
  • Per riferirsi a un dato o a un'istruzione si usa il suo indirizzo: un puntatore. Ad esempio, se il dato 43 si trova all'indirizzo 8007, il puntatore a quel dato vale 8007.

Hardware, firmware e software

Livello Che cos'è
Hardware i componenti fisici del calcolatore
Firmware i microprogrammi nella memoria interna della CU (e programmi come il BIOS)
Software i programmi eseguiti dal calcolatore

Il software si divide in:

  • software di base: serve a tutti gli utenti. Ne fanno parte il sistema operativo e i traduttori dei linguaggi di programmazione (come l'interprete Python);
  • software applicativo: risolve problemi specifici (un foglio di calcolo, un videogioco, il tuo programma).

Il sistema operativo

Il sistema operativo (SO) è l'insieme dei programmi di base che gestisce le risorse hardware e software, in modo semplice ed efficiente per tutti gli utenti. I primi calcolatori non lo avevano: caricare e avviare un programma era compito dell'operatore.

In memoria ci sono in ogni momento cinque aree: buffer, programmi del SO, dati del SO, programmi utente, dati utente. I programmi e i dati del SO sono sempre in memoria; quelli delle applicazioni vengono caricati dal SO su richiesta dell'utente. La CPU esegue i programmi del SO in alternanza con quelli applicativi.

Attenzione ai cicli infiniti

Se un programma utente entra in un ciclo infinito, non restituisce mai il controllo al sistema operativo. Per questo in laboratorio un programma viene fermato dopo 10 secondi.

Il middleware è uno strato di software tra il sistema operativo e le applicazioni. Nasconde le differenze tra reti, hardware, sistemi operativi e linguaggi, e offre alle applicazioni una "macchina generalizzata" con regole di interazione comuni.

Errori frequenti negli esercizi

Errore Cosa succede Come si corregge
dire che gli indirizzi vanno da 1 a 2ᴺ ne conti uno in più e sbagli l'ultimo gli indirizzi vanno da 0 a 2ᴺ − 1: sono 2ᴺ in tutto
confondere lunghezza di parola e bit di indirizzo calcoli la memoria sbagliata m = bit di ogni parola; N = bit dell'indirizzo
calcolare T = f invece di T = 1/f periodo assurdo T = 1 / f; ricorda: 1 GHz → 1 ns
dire che la CPU legge direttamente dal disco concetto errato i dati passano sempre dalla memoria centrale
dire che LOAD cancella il dato letto concetto errato il LOAD copia il dato; è lo STORE che sovrascrive
confondere RAM (accesso casuale) con "memoria volatile" definizione imprecisa RAM descrive il modo di accesso; la volatilità è un'altra proprietà

Esercizi

Esercizio 1 · Le fasi del ciclo. Metti in ordine le fasi e indica in quale fase si usa il registro IR e quando viene aggiornato il PC: Execute, Boot, Decode & Operand Assembly, Fetch.

Soluzione

L'ordine è Boot → Fetch → Decode & Operand Assembly → Execute, poi si torna al Fetch per l'istruzione successiva (il Boot avviene una sola volta, all'inizio).

  • Nel Fetch l'istruzione viene copiata dalla memoria nel registro IR. Subito dopo il prelievo il PC viene aggiornato per puntare alla prossima istruzione.
  • In Decode la CU interpreta il codice operativo contenuto nell'IR e preleva gli operandi.
  • In Execute l'istruzione viene eseguita (ad esempio dall'ALU) e il risultato salvato.

Esercizio 2 · Spazio di indirizzamento. Una CPU ha un Address Bus a 20 bit e un Data Bus a 16 bit. Quanti registri di memoria può indirizzare? Qual è l'ultimo indirizzo? Quanti bit trasferisce in un'unica operazione? Se ogni registro contiene una parola di 16 bit, qual è la capacità massima della memoria in byte?

# Puoi verificare la tua risposta qui
Soluzione
  • Registri indirizzabili: 2²⁰ = 1 048 576.
  • Indirizzi da 0 a 2²⁰ − 1 = 1 048 575.
  • Il Data Bus ha 16 fili: 16 bit (2 byte) alla volta.
  • Capacità: 1 048 576 parole × 2 byte = 2 097 152 byte = 2 048 KB = 2 MB.
registri = 2 ** 20
print(registri, registri - 1)
print(registri * 16 // 8, 'byte')
print(registri * 16 // 8 / 1024 / 1024, 'MB')

Esercizio 3 · Clock. Un processore ha un clock a 2,5 GHz. Calcola il periodo T in nanosecondi. Se ogni istruzione richiede in media 4 cicli di clock, quante istruzioni esegue in un secondo?

# Puoi verificare la tua risposta qui
Soluzione
  • f = 2,5 GHz = 2 500 000 000 Hz.
  • T = 1 / f = 0,0000000004 s = 0,4 ns.
  • Istruzioni al secondo: 2 500 000 000 / 4 = 625 000 000 (625 milioni).
f = 2.5 * 1_000_000_000
T = 1 / f
print('T =', T * 1_000_000_000, 'ns')
print('Istruzioni al secondo:', f / 4)

Verifica