Una cache può essere utilizzata per migliorare le prestazioni di accesso a una data risorsa. Quando ci sono diverse cache per la stessa risorsa, come mostrato nell'immagine, questo può portare a problemi. La coerenza della cache o Cache coherency si riferisce a una serie di modi per assicurarsi che tutte le cache della risorsa abbiano gli stessi dati, e che i dati nelle cache abbiano senso (chiamata integrità dei dati). La coerenza della cache è un caso speciale della coerenza della memoria.
Ci possono essere problemi se ci sono molte cache di una risorsa di memoria comune, poiché i dati nella cache potrebbero non avere più senso, o una cache potrebbe non avere più gli stessi dati delle altre. Un caso comune in cui il problema si verifica è la cache delle CPU in un sistema multiprocessing. Come si può vedere nella figura, se il client superiore ha una copia di un blocco di memoria da una lettura precedente e il client inferiore cambia quel blocco di memoria, il client superiore potrebbe essere lasciato con una cache di memoria non valida, senza saperlo. La coerenza della cache è lì per gestire tali conflitti e mantenere la coerenza tra cache e memoria.
Cause dei problemi di incoerenza
- Più processori o core mantengono copie locali della stessa locazione di memoria per ridurre la latenza degli accessi.
- Un core può aggiornare la propria copia senza aggiornare immediatamente le altre copie presenti nelle cache degli altri core.
- Operazioni di scrittura non coordinate possono lasciare copie diverse in stati diversi (valide, invalide, sporche), portando a letture errate o a risultati non deterministici.
Principi di base e obiettivi
- Assicurare che, per ogni locazione di memoria condivisa, tutte le cache vedano aggiornamenti in modo coerente secondo le regole definite dal protocollo di coerenza.
- Mantenere l’integrità dei dati, cioè evitare che letture successive restituiscano valori obsoleti non riconciliati con le scritture effettuate da altri processor.
- Bilanciare correttezza e prestazioni: i meccanismi devono introdurre il minimo overhead possibile in termini di traffico e latenza.
Meccanismi principali
I meccanismi usati nei sistemi reali ricadono in due grandi famiglie.
- Snooping (o bus-based): ogni cache “annusa” il bus di memoria e osserva le transazioni fatte dagli altri soggetti. Se una cache vede una scrittura sulla locazione che ha in copia, può invalidare o aggiornare la sua copia. Protocollo classico: MESI (Modified, Exclusive, Shared, Invalid).
- Directory-based: una struttura centralizzata o distribuita (directory) tiene traccia di quali cache possiedono copie di ogni blocco di memoria. Le richieste di lettura/scrittura consultano la directory, che coordina invalidazioni o aggiornamenti. È più scalabile dei sistemi basati su snooping quando ci sono molti core.
Strategie di sincronizzazione dei dati
- Write-through: ogni scrittura nella cache viene immediatamente propagata alla memoria principale. Semplice ma genera molto traffico di memoria.
- Write-back: le scritture vengono effettuate solo nella cache e il blocco diventa “sporco” (dirty); la scrittura in memoria avviene solo quando il blocco deve essere rimosso dalla cache. Riduce il traffico ma richiede meccanismi per informare le altre cache (invalida/aggiorna).
- Invalidazione vs Update:
- Invalidazione: quando un core scrive, le copie nelle altre cache vengono invalidate; i lettori successivi devono rileggere dalla memoria o dalla cache del writer.
- Aggiornamento: la scrittura viene propagata alle altre cache che aggiornano la propria copia. Riduce miss di lettura successive ma aumenta il traffico di scrittura.
Protocollo MESI (esempio)
Il protocollo MESI è uno dei più diffusi nelle CPU moderne. Ogni blocco di cache può essere in uno dei quattro stati:
- Modified: la copia nella cache è diversa dalla memoria principale (è sporca) e nessun'altra cache ha la stessa copia.
- Exclusive: la cache ha una copia pulita esclusiva (uguale alla memoria) e nessun altro la possiede.
- Shared: la copia è pulita e può essere presente anche in altre cache.
- Invalid: la copia non è valida.
Problemi correlati e ottimizzazioni
- False sharing: quando due variabili diverse, usate da thread diversi, si trovano nello stesso blocco di cache; scritture indipendenti provocano invalidazioni/incoerenze e degradano le prestazioni. La soluzione è spesso a livello di layout dei dati o padding.
- Ordinamento della memoria: la coerenza della cache non garantisce automaticamente l’ordine visibile delle operazioni a livello di programma; per questo esistono modelli di memoria (strong/weak ordering) e primitive come memory fences/barriers che i compilatori e l’hardware devono rispettare per sincronizzazione corretta.
- Scalabilità: i protocolli basati su snooping scalano male su centinaia o migliaia di core per via del traffico sul bus; le directory distribuite o le gerarchie di cache sono utilizzate per sistemi molto grandi.
Implicazioni pratiche per programmatori
- Usare primitive di sincronizzazione (mutex, atomic, barrier) per evitare condizioni di race; non fare affidamento sul comportamento “magico” delle cache.
- Attenzione al layout della memoria per ridurre il false sharing e migliorare la locality.
- Conoscere il modello di memoria della piattaforma (x86, ARM, ecc.) aiuta a scrivere codice concorrente corretto ed efficiente.
Conclusione
La coerenza della cache è fondamentale nei sistemi multiprocessore per garantire che i dati condivisi rimangano consistenti e affidabili. Esistono diversi meccanismi hardware e software per ottenere la coerenza, ciascuno con compromessi tra correttezza, prestazioni e scalabilità. Comprendere i principi (invalidazione vs aggiornamento, write-back vs write-through, protocolli come MESI, e problemi come il false sharing) è essenziale per progettare sia l’hardware che il software in ambienti concorrenti.

