Il y a déjà beaucoup de bonnes réponses ici qui couvrent de nombreux points saillants, donc je vais juste ajouter quelques problèmes que je n'ai pas vu abordés directement ci-dessus. Autrement dit, cette réponse ne doit pas être considérée comme un ensemble des avantages et des inconvénients, mais plutôt comme un addendum à d'autres réponses ici.
mmap semble magique
En prenant le cas où le fichier est déjà entièrement mises en cache 1 comme la ligne de base 2 , mmap
peut sembler un peu comme la magie :
mmap
ne nécessite qu'un seul appel système pour (potentiellement) mapper le fichier entier, après quoi aucun autre appel système n'est nécessaire.
mmap
ne nécessite pas de copie des données du fichier du noyau vers l'espace utilisateur.
mmap
vous permet d'accéder au fichier "en tant que mémoire", y compris en le traitant avec toutes les astuces avancées que vous pouvez faire contre la mémoire, telles que la vectorisation automatique du compilateur, les intrinsèques SIMD , la prélecture, les routines d'analyse optimisées en mémoire, OpenMP, etc.
Dans le cas où le fichier est déjà dans le cache, cela semble impossible à battre: vous accédez directement au cache des pages du noyau en tant que mémoire et cela ne peut pas aller plus vite que cela.
Eh bien, c'est possible.
mmap n'est pas vraiment magique parce que ...
mmap fonctionne toujours par page
Un coût caché principal de mmap
vs read(2)
(qui est vraiment l'appel système comparable au niveau du système d'exploitation pour la lecture de blocs ) est que mmap
vous devrez faire "un peu de travail" pour chaque page 4K dans l'espace utilisateur, même si elle peut être masquée par le mécanisme d'erreur de page.
Par exemple, une implémentation typique qui ne mmap
contient que le fichier entier devra être défaillante, donc 100 Go / 4K = 25 millions de défauts pour lire un fichier de 100 Go. Maintenant, ce seront des défauts mineurs , mais 25 milliards de défauts de page ne seront toujours pas très rapides. Le coût d'une faute mineure est probablement de l'ordre de 100 nanos dans le meilleur des cas.
mmap s'appuie fortement sur les performances TLB
Maintenant, vous pouvez passer MAP_POPULATE
à mmap
pour lui dire de configurer toutes les tables de pages avant de revenir, il ne devrait donc y avoir aucun défaut de page lors de l'accès. Maintenant, cela a le petit problème qu'il lit également le fichier entier dans la RAM, ce qui va exploser si vous essayez de mapper un fichier de 100 Go - mais ignorons cela pour l'instant 3 . Le noyau doit effectuer un travail par page pour configurer ces tables de pages (apparaît comme l'heure du noyau). Cela finit par être un coût majeur dans l' mmap
approche, et il est proportionnel à la taille du fichier (c'est-à-dire qu'il ne devient pas relativement moins important à mesure que la taille du fichier augmente) 4 .
Enfin, même dans l'espace utilisateur, l'accès à un tel mappage n'est pas exactement gratuit (par rapport aux grands tampons de mémoire ne provenant pas d'un fichier mmap
) - même une fois que les tables de pages sont configurées, chaque accès à une nouvelle page va, conceptuellement, encourir un échec TLB. Étant donné mmap
qu'introduire un fichier signifie utiliser le cache de pages et ses pages 4K, vous engagez à nouveau ce coût 25 millions de fois pour un fichier de 100 Go.
Désormais, le coût réel de ces erreurs TLB dépend au moins des aspects suivants de votre matériel: (a) combien d'entrées 4K TLB vous avez et comment le reste de la mise en cache de traduction fonctionne (b) la qualité de la prélecture matérielle avec le TLB - par exemple, la prélecture peut-elle déclencher un parcours de page? (c) à quelle vitesse et à quel point le matériel de marche de page est parallèle. Sur les processeurs Intel x86 haut de gamme modernes, le matériel de marche de page est en général très solide: il y a au moins 2 marcheurs de page parallèles, une marche de page peut se produire en même temps que l'exécution continue et la prélecture matérielle peut déclencher une marche de page. Ainsi, l'impact du TLB sur une charge de lecture en continu est assez faible - et une telle charge fonctionnera souvent de la même manière quelle que soit la taille de la page. Cependant, les autres matériels sont généralement bien pires!
read () évite ces pièges
L' read()
appel syscall, qui sous-tend généralement les appels de type "lecture de bloc" proposés, par exemple en C, C ++ et dans d'autres langages, présente un inconvénient majeur dont tout le monde est bien conscient:
- Chaque
read()
appel de N octets doit copier N octets du noyau vers l'espace utilisateur.
D'un autre côté, cela évite la plupart des coûts ci-dessus - vous n'avez pas besoin de mapper 25 millions de pages 4K dans l'espace utilisateur. Vous pouvez généralement malloc
utiliser un seul petit tampon dans l'espace utilisateur et le réutiliser à plusieurs reprises pour tous vos read
appels. Du côté du noyau, il n'y a presque aucun problème avec les pages 4K ou les ratés TLB car toute la RAM est généralement mappée de manière linéaire en utilisant quelques très grandes pages (par exemple, des pages de 1 Go sur x86), de sorte que les pages sous-jacentes du cache de page sont couvertes très efficacement dans l'espace noyau.
Donc, en gros, vous avez la comparaison suivante pour déterminer laquelle est la plus rapide pour une seule lecture d'un gros fichier:
Le travail supplémentaire par page impliqué par l' mmap
approche est-il plus coûteux que le travail par octet de copie du contenu du fichier du noyau vers l'espace utilisateur impliqué par l'utilisation read()
?
Sur de nombreux systèmes, ils sont en fait à peu près équilibrés. Notez que chacun évolue avec des attributs complètement différents du matériel et de la pile du système d'exploitation.
En particulier, l' mmap
approche devient relativement plus rapide lorsque:
- Le système d'exploitation a une gestion rapide des défauts mineurs et en particulier des optimisations de groupage de défauts mineurs tels que le contournement des défauts.
- Le système d'exploitation a une bonne
MAP_POPULATE
implémentation qui peut traiter efficacement de grandes cartes dans les cas où, par exemple, les pages sous-jacentes sont contiguës dans la mémoire physique.
- Le matériel a de bonnes performances de traduction de page, telles que des TLB volumineux, des TLB rapides de deuxième niveau, des pages-walkers rapides et parallèles, une bonne interaction de prélecture avec la traduction, etc.
... tandis que l' read()
approche devient relativement plus rapide lorsque:
- L'
read()
appel système a de bonnes performances de copie. Par exemple, de bonnes copy_to_user
performances côté noyau.
- Le noyau a un moyen efficace (par rapport à l'utilisateur) de mapper la mémoire, par exemple, en utilisant seulement quelques grandes pages avec un support matériel.
- Le noyau a des appels système rapides et un moyen de conserver les entrées TLB du noyau à travers les appels système.
Les facteurs matériels ci-dessus varient énormément selon les plates-formes, même au sein de la même famille (par exemple, au sein des générations x86 et en particulier des segments de marché) et certainement d'une architecture à l'autre (par exemple, ARM vs x86 vs PPC).
Les facteurs du système d'exploitation changent également, avec diverses améliorations des deux côtés provoquant un grand saut de la vitesse relative pour une approche ou l'autre. Une liste récente comprend:
- Ajout de défaut, décrit ci-dessus, qui aide vraiment le
mmap
cas sans MAP_POPULATE
.
- Ajout de
copy_to_user
méthodes rapides dans arch/x86/lib/copy_user_64.S
, par exemple, en utilisant REP MOVQ
quand il est rapide, ce qui aide vraiment le read()
cas.
Mise à jour après Spectre et Meltdown
Les atténuations des vulnérabilités Spectre et Meltdown ont considérablement augmenté le coût d'un appel système. Sur les systèmes que j'ai mesurés, le coût d'un appel système "ne rien faire" (qui est une estimation de la surcharge pure de l'appel système, en dehors de tout travail réel effectué par l'appel) est passé d'environ 100 ns sur un système Linux moderne à environ 700 ns. En outre, en fonction de votre système, le correctif d' isolement de table de page spécifiquement pour Meltdown peut avoir des effets en aval supplémentaires en dehors du coût d'appel système direct en raison de la nécessité de recharger les entrées TLB.
Tout ceci est un inconvénient relatif pour les read()
méthodes basées par rapport aux mmap
méthodes basées, puisque les read()
méthodes doivent faire un appel système pour chaque valeur de «taille de tampon» de données. Vous ne pouvez pas augmenter arbitrairement la taille de la mémoire tampon pour amortir ce coût, car l'utilisation de tampons volumineux fonctionne généralement moins bien puisque vous dépassez la taille L1 et que vous souffrez donc constamment d'erreurs de cache.
D'autre part, avec mmap
, vous pouvez mapper dans une grande région de mémoire avec MAP_POPULATE
et y accéder efficacement, au prix d'un seul appel système.
1 Cela inclut plus ou moins également le cas où le fichier n'était pas entièrement mis en cache au départ, mais où la lecture anticipée du système d'exploitation est suffisamment bonne pour le faire apparaître ainsi (c'est-à-dire que la page est généralement mise en cache au moment où vous le veux). Ceci est une question subtile mais parce que la façon dont fonctionne la lecture anticipée est souvent tout à fait différente entre mmap
et read
appels, et peuvent être ajustés par des appels « conseiller les » comme décrit dans 2 .
2 ... parce que si le fichier n'est pas mis en cache, votre comportement sera complètement dominé par des problèmes d'E / S, y compris à quel point votre modèle d'accès est sympathique au matériel sous-jacent - et tous vos efforts devraient être pour garantir qu'un tel accès est aussi sympathique que possible, par exemple via l'utilisation de madvise
ou des fadvise
appels (et quels que soient les changements de niveau d'application que vous pouvez apporter pour améliorer les modèles d'accès).
3 Vous pouvez contourner cela, par exemple, en insérant séquentiellement mmap
dans des fenêtres de plus petite taille, par exemple 100 Mo.
4 En fait, il s'avère que l' MAP_POPULATE
approche (au moins une combinaison matérielle / système d'exploitation) est légèrement plus rapide que de ne pas l'utiliser, probablement parce que le noyau utilise la solution de panne - le nombre réel de défauts mineurs est donc réduit d'un facteur 16 ou alors.
mmap()
c'est 2 à 6 fois plus rapide que d'utiliser des appels système, par exempleread()
.