Prestazioni di codice orientato all'ADT a singola assegnazione su CPU moderne


32

Lavorare in dati immutabili con singole assegnazioni ha l'effetto ovvio di richiedere più memoria, si potrebbe presumere, perché si creano costantemente nuovi valori (anche se i compilatori sotto le copertine fanno trucchi con i puntatori per rendere questo meno un problema).

Ma ho sentito alcune volte che le perdite in termini di prestazioni sono compensate dai guadagni nel modo in cui la CPU (in particolare il suo controller di memoria) può trarre vantaggio dal fatto che la memoria non è mutata (tanto).

Speravo che qualcuno potesse far luce su come questo è vero (o se non lo è?).

In un commento su un altro post è stato menzionato che i tipi di dati astratti (ADT) hanno a che fare con questo, il che mi ha reso ancora più curioso, in che modo gli ADT influiscono in modo specifico sul modo in cui la CPU gestisce la memoria? Questo è tuttavia un aspetto a parte, per lo più sono solo interessato a come la purezza del linguaggio influisce necessariamente sulle prestazioni della CPU e delle sue cache ecc.


2
questo è utile soprattutto nel multithreading, in cui un lettore può catturare atomicamente un'istantanea ed essere al sicuro sapendo che non muterà mentre lo legge
maniaco del cricchetto

@ratchetfreak Capisco dal punto di vista della programmazione che il tuo codice ottiene maggiori garanzie di sicurezza, ma la mia curiosità riguarda il controller di memoria sulla CPU e il modo in cui questo comportamento conta (o se non lo fa) come ho sentito affermazioni bandite circa una mano piena di volte che diceva che era più efficiente per il controller di memoria, e non conosco abbastanza bene i dettagli di basso livello per dire se o come ciò potrebbe essere vero.
Jimmy Hoffa,

Anche se fosse vero, non penserei che una minore modifica della memoria sia il punto di forza per l'immutabilità. La memoria è lì per essere modificata, dopo tutto, e CPU e gestori di memoria sono diventati abbastanza bravi nel corso degli anni.
Rein Henrichs,

1
Vorrei anche sottolineare che l'efficienza della memoria non deve necessariamente dipendere dalle ottimizzazioni del compilatore quando si utilizzano strutture immutabili. In questo esempio let a = [1,2,3] in let b = 0:a in (a, b, (-1):c)condivisione riduce i requisiti di memoria, ma dipende dalla definizione di (:)e []non il compilatore. Credo? Non sono sicura di questo.

Risposte:


28

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:

  1. Tutti i core interrompono l'elaborazione - per evitare di scrivere accidentalmente nella cache.
  2. 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.
  3. 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.
  4. 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.



Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.