La CPU (in particolare il suo controller di memoria) può trarre vantaggio dal fatto che la memoria non è mutata
Il vantaggio è che questo fatto salva il compilatore dall'uso delle istruzioni membar quando si accede ai dati.
Una barriera di memoria, nota anche come membar, memoria di recinzione o istruzione di recinzione, è un tipo di istruzione di barriera che fa sì che un'unità di elaborazione centrale (CPU) o un compilatore imponga un vincolo di ordinazione sulle operazioni di memoria emesse prima e dopo l'istruzione di barriera. Ciò significa in genere che determinate operazioni sono garantite per essere eseguite prima della barriera e altre dopo.
Le barriere di memoria sono necessarie perché la maggior parte delle CPU moderne impiega ottimizzazioni delle prestazioni che possono comportare un'esecuzione fuori servizio. Questo riordino delle operazioni di memoria (carica e archivia) normalmente passa inosservato all'interno di un singolo thread di esecuzione, ma può causare comportamenti imprevedibili in programmi e driver di dispositivo simultanei se non controllati attentamente ...
Vedete, quando si accede ai dati da thread diversi, nella CPU multi-core procede come segue: thread diversi vengono eseguiti su core diversi, ognuno dei quali utilizza la propria cache (locale al proprio core), una copia di una cache globale.
Se i dati sono mutabili e il programmatore ha bisogno che sia coerente tra thread diversi, è necessario prendere misure per garantire la coerenza. Per il programmatore, ciò significa utilizzare costrutti di sincronizzazione quando accedono (ad esempio, leggono) i dati in un particolare thread.
Per il compilatore, il costrutto di sincronizzazione nel codice significa che è necessario inserire un'istruzione membar per assicurarsi che le modifiche apportate alla copia dei dati su uno dei core siano propagate correttamente ("pubblicate"), per garantire che le cache su altri core avere la stessa copia (aggiornata).
Semplificando un po ' vedi la nota sotto , ecco cosa succede al processore multi-core per membar:
- Tutti i core interrompono l'elaborazione - per evitare di scrivere accidentalmente nella cache.
- Tutti gli aggiornamenti apportati alle cache locali vengono riscritti in quello globale, per garantire che la cache globale contenga i dati più recenti. Questo richiede del tempo.
- I dati aggiornati vengono riscritti dalla cache globale a quelli locali, per garantire che le cache locali contengano i dati più recenti. Questo richiede del tempo.
- Tutti i core riprendono l'esecuzione.
Vedete, tutti i core non stanno facendo nulla mentre i dati vengono copiati avanti e indietro tra le cache globali e locali . Ciò è necessario per garantire che i dati mutabili siano sincronizzati correttamente (thread-safe). Se ci sono 4 core, tutti e 4 si fermano e attendono che le cache vengano sincronizzate. Se ce ne sono 8, tutte e 8 si fermano. Se ce ne sono 16 ... beh, hai 15 core che non fanno esattamente nulla mentre aspettano cose da fare in uno di questi.
Ora, vediamo cosa succede quando i dati sono immutabili? Non importa a quale thread accede, è garantito che sia lo stesso. Per il programmatore, ciò significa che non è necessario inserire costrutti di sincronizzazione quando accedono (leggono) i dati in un determinato thread.
Per il compilatore, questo a sua volta significa che non è necessario inserire un'istruzione membar .
Di conseguenza, l'accesso ai dati non deve arrestare i core e attendere che i dati vengano scritti avanti e indietro tra le cache globali e locali. Questo è un vantaggio del fatto che la memoria non è mutata .
Nota che la spiegazione in qualche modo semplificata sopra elimina alcuni effetti negativi più complicati della mutabilità dei dati, ad esempio sul pipelining . Al fine di garantire l'ordinamento richiesto, la CPU deve invalidare le linee guida interessate dalle modifiche ai dati: questa è un'altra penalità per le prestazioni. Se questo viene implementato dall'invalidazione semplice (e quindi affidabile :) di tutte le condutture, l'effetto negativo viene ulteriormente amplificato.