Hier gibt es bereits viele gute Antworten, die viele der wichtigsten Punkte abdecken. Daher möchte ich nur einige Punkte hinzufügen, die ich nicht direkt oben angesprochen habe. Das heißt, diese Antwort sollte nicht als umfassend für die Vor- und Nachteile angesehen werden, sondern als Ergänzung zu anderen Antworten hier.
mmap scheint magisch
Wenn Sie den Fall, in dem die Datei bereits vollständig zwischengespeichert ist 1 als Basis 2 haben , mmapals magisch erscheinen :
mmap Es ist nur ein Systemaufruf erforderlich, um (möglicherweise) die gesamte Datei zuzuordnen. Danach sind keine weiteren Systemaufrufe erforderlich.
mmap erfordert keine Kopie der Dateidaten vom Kernel in den User-Space.
mmapErmöglicht den Zugriff auf die Datei "als Speicher", einschließlich der Verarbeitung mit allen erweiterten Tricks, die Sie gegen den Speicher ausführen können, z. B. automatische Vektorisierung des Compilers, SIMD- Intrinsics, Prefetching, optimierte In-Memory-Parsing-Routinen, OpenMP usw.
Für den Fall, dass sich die Datei bereits im Cache befindet, scheint es unmöglich zu sein: Sie greifen einfach direkt auf den Kernel-Seiten-Cache als Speicher zu und es kann nicht schneller werden.
Nun, das kann es.
mmap ist eigentlich keine Magie, weil ...
mmap arbeitet immer noch pro Seite
Ein primärer versteckter Preis von mmapvs read(2)(was eigentlich der vergleichbare Systemaufruf auf Betriebssystemebene zum Lesen von Blöcken ist ) besteht darin, dass mmapSie für jede 4K-Seite im Benutzerbereich "etwas Arbeit" erledigen müssen, auch wenn sie möglicherweise von der Seitenfehlermechanismus.
Zum Beispiel muss eine typische Implementierung, die nur mmapdie gesamte Datei enthält, einen Fehler verursachen, sodass 100 GB / 4K = 25 Millionen Fehler zum Lesen einer 100-GB-Datei vorliegen. Nun, dies werden kleinere Fehler sein , aber 25 Milliarden Seitenfehler werden immer noch nicht superschnell sein. Die Kosten für einen kleinen Fehler liegen wahrscheinlich im besten Fall bei 100 Nanos.
mmap hängt stark von der TLB-Leistung ab
Jetzt können Sie an übergeben MAP_POPULATE, mmapum anzuweisen, dass alle Seitentabellen eingerichtet werden sollen, bevor Sie zurückkehren, damit beim Zugriff keine Seitenfehler auftreten. Dies hat das kleine Problem, dass es auch die gesamte Datei in den Arbeitsspeicher liest, was explodieren wird, wenn Sie versuchen, eine 100-GB-Datei zuzuordnen - aber lassen Sie uns dies vorerst ignorieren 3 . Der Kernel muss pro Seite arbeiten , um diese Seitentabellen einzurichten (wird als Kernelzeit angezeigt). Dies ist ein erheblicher Kostenfaktor für den mmapAnsatz und proportional zur Dateigröße (dh er wird mit zunehmender Dateigröße nicht weniger wichtig) 4 .
Selbst im Benutzerbereich ist der Zugriff auf eine solche Zuordnung nicht gerade kostenlos (im Vergleich zu großen Speicherpuffern, die nicht aus einer dateibasierten Zuordnung stammen mmap). Selbst wenn die Seitentabellen eingerichtet sind, wird jeder Zugriff auf eine neue Seite ausgeführt. konzeptionell entsteht ein TLB-Fehler. Da das mmapErstellen einer Datei die Verwendung des Seitencaches und seiner 4K-Seiten bedeutet, fallen für eine 100-GB-Datei erneut 25 Millionen Mal Kosten an.
Nun hängen die tatsächlichen Kosten dieser TLB-Fehler stark von mindestens den folgenden Aspekten Ihrer Hardware ab: (a) wie viele 4K-TLB-Enties Sie haben und wie der Rest des Übersetzungs-Caching funktioniert (b) wie gut Hardware-Prefetch funktioniert mit dem TLB - kann zB Prefetch einen Seitenlauf auslösen? (c) wie schnell und wie parallel die Page-Walking-Hardware ist. Auf modernen High-End-x86-Intel-Prozessoren ist die Page-Walk-Hardware im Allgemeinen sehr stark: Es gibt mindestens zwei parallele Page-Walker, ein Page-Walk kann gleichzeitig mit der fortgesetzten Ausführung erfolgen, und Hardware-Prefetching kann einen Page-Walk auslösen. Daher ist die Auswirkung des TLB auf eine Streaming- Leselast relativ gering - und eine solche Last wird unabhängig von der Seitengröße häufig ähnlich ausgeführt. Andere Hardware ist jedoch normalerweise viel schlechter!
read () vermeidet diese Fallstricke
Der read()Syscall, der im Allgemeinen den "Block Read" -Aufrufen zugrunde liegt, die z. B. in C, C ++ und anderen Sprachen angeboten werden, hat einen Hauptnachteil, den jeder kennt:
- Jeder
read()Aufruf von N Bytes muss N Bytes vom Kernel in den Benutzerbereich kopieren.
Auf der anderen Seite werden die meisten der oben genannten Kosten vermieden - Sie müssen nicht 25 Millionen 4K-Seiten in den Benutzerbereich abbilden. Normalerweise können Sie malloceinen einzelnen Puffer, einen kleinen Puffer im Benutzerbereich, verwenden und diesen wiederholt für alle Ihre readAnrufe wiederverwenden . Auf der Kernelseite gibt es fast kein Problem mit 4K-Seiten oder TLB-Fehlern, da der gesamte RAM normalerweise linear mit einigen sehr großen Seiten (z. B. 1 GB Seiten auf x86) zugeordnet wird, sodass die zugrunde liegenden Seiten im Seitencache abgedeckt sind sehr effizient im Kernelraum.
Grundsätzlich haben Sie also den folgenden Vergleich, um festzustellen, welche für einen einzelnen Lesevorgang einer großen Datei schneller ist:
Ist die zusätzliche Arbeit pro Seite, die durch den mmapAnsatz impliziert wird, teurer als die Arbeit pro Byte beim Kopieren von Dateiinhalten vom Kernel in den Benutzerbereich, die durch die Verwendung impliziert wird read()?
Auf vielen Systemen sind sie tatsächlich ungefähr ausgeglichen. Beachten Sie, dass jeder mit völlig unterschiedlichen Attributen der Hardware und des Betriebssystemstapels skaliert.
Insbesondere wird der mmapAnsatz relativ schneller, wenn:
- Das Betriebssystem verfügt über eine schnelle Behandlung kleinerer Fehler und insbesondere über Bulk-Optimierungen kleiner Fehler wie Fehlerumgehung.
- Das Betriebssystem verfügt über eine gute
MAP_POPULATEImplementierung, mit der große Karten effizient verarbeitet werden können, wenn beispielsweise die zugrunde liegenden Seiten im physischen Speicher zusammenhängend sind.
- Die Hardware bietet eine starke Leistung bei der Seitenübersetzung, z. B. große TLBs, schnelle TLBs der zweiten Ebene, schnelle und parallele Page-Walker, eine gute Prefetch-Interaktion mit der Übersetzung usw.
... während der read()Ansatz relativ schneller wird, wenn:
- Der
read()Syscall hat eine gute Kopierleistung. ZB gute copy_to_userLeistung auf der Kernelseite.
- Der Kernel verfügt über eine effiziente (im Verhältnis zum Benutzerland) Möglichkeit, Speicher zuzuordnen, z. B. indem nur wenige große Seiten mit Hardwareunterstützung verwendet werden.
- Der Kernel verfügt über schnelle Systemaufrufe und eine Möglichkeit, Kernel-TLB-Einträge über Systemaufrufe hinweg zu speichern.
Die Hardware - Faktoren , die oben variieren wild über verschiedene Plattformen hinweg, sogar innerhalb der gleichen Familie (zB innerhalb x86 Generationen und vor allem Marktsegmente) und auf jeden Fall über Architekturen (zB ARM vs x86 vs PPC).
Auch die OS-Faktoren ändern sich ständig, wobei verschiedene Verbesserungen auf beiden Seiten bei dem einen oder anderen Ansatz zu einem starken Anstieg der Relativgeschwindigkeit führen. Eine aktuelle Liste enthält:
- Hinzufügen der oben beschriebenen Fehlerbehebung, die den
mmapFall ohne wirklich hilft MAP_POPULATE.
- Hinzufügen von Fast-Path-
copy_to_userMethoden arch/x86/lib/copy_user_64.S, z. B. REP MOVQwenn es schnell ist, was dem read()Fall wirklich hilft .
Update nach Spectre und Meltdown
Die Abschwächung der Schwachstellen Spectre und Meltdown erhöhte die Kosten eines Systemaufrufs erheblich. Auf den Systemen, die ich gemessen habe, gingen die Kosten für einen Systemaufruf "nichts tun" (der eine Schätzung des reinen Overheads des Systemaufrufs darstellt, abgesehen von der tatsächlichen Arbeit, die durch den Aufruf ausgeführt wurde) von ungefähr 100 ns auf einen typischen Wert modernes Linux-System bis ca. 700 ns. Abhängig von Ihrem System kann der speziell für Meltdown festgelegte Seitentabellen-Isolations- Fix neben den direkten Systemaufrufkosten zusätzliche Downstream-Effekte haben, da TLB-Einträge neu geladen werden müssen.
All dies ist ein relativer Nachteil für read()basierte Methoden im Vergleich zu mmapbasierten Methoden, da read()Methoden für jede Datenmenge mit "Puffergröße" einen Systemaufruf ausführen müssen. Sie können die Puffergröße nicht willkürlich erhöhen, um diese Kosten zu amortisieren, da die Verwendung großer Puffer normalerweise schlechter abschneidet, da Sie die L1-Größe überschreiten und daher ständig unter Cache-Fehlern leiden.
Auf der anderen Seite können Sie mit mmapeine große Speicherregion mit MAP_POPULATEund nur effizientem Zugriff auf Kosten eines einzigen Systemaufrufs abbilden.
1 Dies schließt mehr oder weniger auch den Fall ein, in dem die Datei zunächst nicht vollständig zwischengespeichert war, das Vorauslesen des Betriebssystems jedoch gut genug ist, um es so erscheinen zu lassen (dh die Seite wird normalerweise zu dem Zeitpunkt zwischengespeichert, zu dem Sie sich befinden will es). Dies ist jedoch ein subtiles Problem, da die Art und Weise, wie das Vorauslesen funktioniert, zwischen mmapund readAnrufen häufig sehr unterschiedlich ist und durch "Beratung" -Anrufe weiter angepasst werden kann, wie in 2 beschrieben .
2 ... denn wenn die Datei nicht zwischengespeichert wird, wird Ihr Verhalten vollständig von E / A-Bedenken dominiert, einschließlich der Sympathie Ihres Zugriffsmusters für die zugrunde liegende Hardware - und Sie sollten sich alle Mühe geben, um sicherzustellen, dass ein solcher Zugriff so sympathisch ist wie möglich, z. B. durch Verwendung von madviseoder fadviseAufrufe (und welche Änderungen auf Anwendungsebene Sie vornehmen können, um die Zugriffsmuster zu verbessern).
3 Sie können dies umgehen, indem Sie beispielsweise mmapFenster kleinerer Größe, z. B. 100 MB , nacheinander eingeben.
4 Tatsächlich stellt sich heraus, dass der MAP_POPULATEAnsatz (mindestens eine Kombination aus Hardware und Betriebssystem) nur geringfügig schneller ist als die Nichtverwendung , wahrscheinlich weil der Kernel eine Fehlerbehebung verwendet. Daher wird die tatsächliche Anzahl kleinerer Fehler um den Faktor 16 reduziert oder so.
mmap()ist es 2-6 mal schneller als mit Syscalls, zread().