Ci sono già molte buone risposte qui che coprono molti dei punti salienti, quindi aggiungerò solo un paio di problemi che non ho visto affrontati direttamente sopra. Cioè, questa risposta non dovrebbe essere considerata una panoramica di pro e contro, ma piuttosto un addendum ad altre risposte qui.
mmap sembra magico
Prendendo il caso in cui il file è già completamente memorizzato nella cache 1 come base 2 , mmap
potrebbe sembrare praticamente magico :
mmap
richiede solo 1 chiamata di sistema per (potenzialmente) mappare l'intero file, dopo di che non sono necessarie altre chiamate di sistema.
mmap
non richiede una copia dei dati del file dal kernel allo spazio utente.
mmap
ti consente di accedere al file "come memoria", incluso l'elaborazione con qualsiasi trucco avanzato che puoi fare contro la memoria, come vettorializzazione automatica del compilatore, intrinseci SIMD , prefetch, routine di analisi in memoria ottimizzate, OpenMP, ecc.
Nel caso in cui il file sia già nella cache, sembra impossibile da battere: basta accedere direttamente alla cache della pagina del kernel come memoria e non può essere più veloce di così.
Beh, può.
mmap non è in realtà magico perché ...
mmap funziona ancora per pagina
Un costo nascosto primario di mmap
vs read(2)
(che è in realtà il comparabile syscall a livello di sistema operativo per i blocchi di lettura ) è che con mmap
te dovrai fare "un po 'di lavoro" per ogni pagina 4K nello spazio utente, anche se potrebbe essere nascosto dal meccanismo di errore di pagina.
Ad esempio, un'implementazione tipica che è solo mmap
l'intero file dovrà essere inserita in modo errato, quindi 100 GB / 4K = 25 milioni di errori per leggere un file da 100 GB. Ora, questi saranno piccoli errori , ma 25 miliardi di errori di pagina non saranno ancora super veloci. Il costo di un errore minore è probabilmente nel centinaio di nanos nel migliore dei casi.
mmap fa molto affidamento sulle prestazioni TLB
Ora, puoi passare MAP_POPULATE
a mmap
per dirgli di impostare tutte le tabelle delle pagine prima di tornare, quindi non dovrebbero esserci errori di pagina durante l'accesso. Ora, questo ha il piccolo problema che legge anche l'intero file nella RAM, che esploderà se provi a mappare un file da 100 GB, ma per ora ignoralo 3 . Il kernel deve eseguire il lavoro per pagina per impostare queste tabelle di pagine (visualizzate come tempo del kernel). Questo finisce per essere un costo importante mmap
nell'approccio, ed è proporzionale alla dimensione del file (cioè, non diventa relativamente meno importante con l'aumentare della dimensione del file) 4 .
Infine, anche nello spazio utente l'accesso a tale mappatura non è esattamente gratuito (rispetto ai buffer di memoria di grandi dimensioni che non provengono da un file-based mmap
) - anche una volta impostate le tabelle delle pagine, ogni accesso a una nuova pagina verrà, concettualmente, incorre in una mancanza TLB. Poiché la mmap
creazione di un file implica l'utilizzo della cache delle pagine e delle sue pagine 4K, è necessario sostenere nuovamente questo costo 25 milioni di volte per un file da 100 GB.
Ora, il costo effettivo di questi mancati TLB dipende in gran parte almeno dai seguenti aspetti dell'hardware: (a) quante entità TLB 4K hai e come funziona il resto della cache di traduzione (b) con che cosa gestisce il prefetch hardware con il TLB - ad esempio, è possibile eseguire il prefetch per attivare una camminata di pagina? (c) quanto veloce e parallelo è l'hardware di camminata della pagina. Sui moderni processori Intel x86 di fascia alta, l'hardware di camminata di pagina è in generale molto forte: ci sono almeno 2 camminatori di pagine parallele, una camminata di pagina può avvenire in concomitanza con l'esecuzione continua e il prefetching dell'hardware può innescare una camminata di pagina. Pertanto, l'impatto di TLB su un carico in lettura in streaming è piuttosto basso e tale carico spesso si comporta in modo simile indipendentemente dalle dimensioni della pagina. L'altro hardware di solito è molto peggio!
read () evita queste insidie
Il read()
syscall, che è ciò che generalmente sta alla base delle chiamate di tipo "blocco letto" offerte, ad esempio, in C, C ++ e altre lingue, presenta uno svantaggio principale di cui tutti sono ben consapevoli:
- Ogni
read()
chiamata di N byte deve copiare N byte dal kernel nello spazio utente.
D'altra parte, evita la maggior parte dei costi di cui sopra - non è necessario mappare in 25 milioni di pagine 4K nello spazio utente. Di solito è possibile malloc
un singolo buffer piccolo buffer nello spazio utente e riutilizzarlo ripetutamente per tutte le read
chiamate. Dal lato del kernel, non c'è quasi nessun problema con le pagine 4K o i mancati TLB perché tutta la RAM è solitamente mappata linearmente usando poche pagine molto grandi (ad esempio, pagine da 1 GB su x86), quindi le pagine sottostanti nella cache delle pagine sono coperte molto efficientemente nello spazio del kernel.
Quindi fondamentalmente hai il seguente confronto per determinare quale è più veloce per una singola lettura di un file di grandi dimensioni:
Il lavoro extra per pagina implicato mmap
dall'approccio è più costoso del lavoro per byte della copia del contenuto dei file dal kernel nello spazio utente implicito usando read()
?
Su molti sistemi, in realtà sono approssimativamente bilanciati. Si noti che ognuno ridimensiona con attributi completamente diversi dello stack hardware e del sistema operativo.
In particolare, l' mmap
approccio diventa relativamente più veloce quando:
- Il sistema operativo ha una rapida gestione dei guasti minori e in particolare ottimizzazioni di carica per guasti minori come guasti.
- Il sistema operativo ha una buona
MAP_POPULATE
implementazione che può elaborare in modo efficiente mappe di grandi dimensioni nei casi in cui, ad esempio, le pagine sottostanti sono contigue nella memoria fisica.
- L'hardware offre ottime prestazioni di traduzione delle pagine, come TLB di grandi dimensioni, TLB veloci di secondo livello, page walker veloci e paralleli, buona interazione di prefetch con la traduzione e così via.
... mentre l' read()
approccio diventa relativamente più veloce quando:
- Il
read()
syscall ha buone prestazioni di copia. Ad esempio, buone copy_to_user
prestazioni sul lato kernel.
- Il kernel ha un modo efficiente (relativamente all'area utente) per mappare la memoria, ad esempio usando solo poche pagine di grandi dimensioni con supporto hardware.
- Il kernel ha syscalls veloci e un modo per mantenere le voci TLB del kernel attraverso syscalls.
I fattori hardware sopra descritti variano notevolmente tra piattaforme diverse, anche all'interno della stessa famiglia (ad esempio, entro x86 generazioni e in particolare segmenti di mercato) e sicuramente tra architetture (ad esempio, ARM vs x86 vs PPC).
Anche i fattori del sistema operativo continuano a cambiare, con vari miglioramenti su entrambi i lati che causano un grande salto nella velocità relativa per un approccio o l'altro. Un elenco recente include:
- Aggiunta di un errore, descritto sopra, che aiuta davvero il
mmap
caso senza MAP_POPULATE
.
- Aggiunta di
copy_to_user
metodi di percorso rapido arch/x86/lib/copy_user_64.S
, ad esempio, REP MOVQ
quando è veloce, il che aiuta davvero il read()
caso.
Aggiornamento dopo Spectre e Meltdown
Le mitigazioni delle vulnerabilità di Spectre e Meltdown hanno aumentato notevolmente il costo di una chiamata di sistema. Sui sistemi che ho misurato, il costo di una chiamata di sistema "non fare nulla" (che è una stima del puro sovraccarico della chiamata di sistema, a parte qualsiasi lavoro effettivo svolto dalla chiamata) è passato da circa 100 ns su un tipico moderno sistema Linux a circa 700 ns. Inoltre, a seconda del sistema in uso, la correzione dell'isolamento della tabella delle pagine appositamente per Meltdown può avere effetti downstream aggiuntivi oltre al costo delle chiamate dirette del sistema a causa della necessità di ricaricare le voci TLB.
Tutto ciò rappresenta uno svantaggio relativo per i read()
metodi basati rispetto ai mmap
metodi basati, poiché i read()
metodi devono effettuare una chiamata di sistema per ciascun valore di "dimensioni del buffer" di dati. Non è possibile aumentare in modo arbitrario la dimensione del buffer per ammortizzare questo costo poiché l'utilizzo di buffer di grandi dimensioni di solito ha prestazioni peggiori poiché si supera la dimensione L1 e quindi si verificano costantemente perdite di cache.
D'altra parte, con mmap
, è possibile mappare in una vasta area di memoria con MAP_POPULATE
e accedervi in modo efficiente, al costo di una sola chiamata di sistema.
1 Questo include più o meno anche il caso in cui il file non era completamente memorizzato nella cache per iniziare, ma in cui il sistema operativo read-ahead è abbastanza buono da farlo apparire così (cioè, la pagina viene solitamente memorizzata nella cache quando lo voglio). Questo è un problema sottile, tuttavia, poiché il modo in cui funziona il read-ahead è spesso abbastanza diverso tra mmap
e read
chiamate e può essere ulteriormente regolato da chiamate "avvisare" come descritto in 2 .
2 ... perché se il file non viene memorizzato nella cache, il tuo comportamento sarà completamente dominato dalle preoccupazioni di I / O, incluso quanto sia comprensivo il tuo modello di accesso all'hardware sottostante - e tutti i tuoi sforzi dovrebbero essere nel garantire che tale accesso sia comprensivo come possibile, ad es. tramite l'uso madvise
o le fadvise
chiamate (e qualsiasi modifica del livello dell'applicazione sia possibile apportare per migliorare i modelli di accesso).
3 Potresti aggirare il problema, ad esempio mmap
inserendo sequenzialmente finestre di dimensioni inferiori, ad esempio 100 MB.
4 In effetti, si scopre che l' MAP_POPULATE
approccio è (almeno una combinazione hardware / sistema operativo) solo leggermente più veloce rispetto al non utilizzo, probabilmente perché il kernel sta usando un errore - quindi il numero effettivo di errori minori è ridotto di un fattore 16 o così.
mmap()
è 2-6 volte più veloce rispetto all'utilizzo di syscalls, ad esread()
.