mmap () vs blocs de lecture

191

Je travaille sur un programme qui traitera des fichiers dont la taille pourrait potentiellement atteindre 100 Go ou plus. Les fichiers contiennent des ensembles d'enregistrements de longueur variable. J'ai une première implémentation opérationnelle et je cherche maintenant à améliorer les performances, en particulier à faire des E / S plus efficacement puisque le fichier d'entrée est analysé plusieurs fois.

Existe-t-il une règle de base pour utiliser mmap()ou lire dans des blocs via la fstreambibliothèque C ++ ? Ce que j'aimerais faire, c'est lire de gros blocs du disque dans une mémoire tampon, traiter des enregistrements complets à partir de la mémoire tampon, puis en savoir plus.

Le mmap()code pourrait potentiellement devenir très compliqué car mmaples blocs 'd doivent se trouver sur les limites de la taille de la page (à ma connaissance) et les enregistrements pourraient potentiellement passer à travers les limites de la page. Avec fstreams, je peux simplement chercher le début d'un enregistrement et recommencer la lecture, car nous ne sommes pas limités à la lecture de blocs qui se trouvent sur des limites de la taille d'une page.

Comment puis-je choisir entre ces deux options sans rédiger d'abord une implémentation complète? Des règles empiriques (par exemple, mmap()est-ce 2x plus rapide) ou des tests simples?

jbl
la source
1
C'est une lecture intéressante: medium.com/@sasha_f/… Dans les expériences, mmap()c'est 2 à 6 fois plus rapide que d'utiliser des appels système, par exemple read().
mplattner

Réponses:

215

J'essayais de trouver le dernier mot sur les performances mmap / read sous Linux et je suis tombé sur un joli post ( lien ) sur la liste de diffusion du noyau Linux. Il date de 2000, il y a donc eu de nombreuses améliorations aux E / S et à la mémoire virtuelle dans le noyau depuis lors, mais cela explique bien la raison pour laquelle mmapou readpourrait être plus rapide ou plus lent.

  • Un appel à mmapa plus de frais généraux que read(tout comme epolla plus de frais généraux que poll, qui a plus de frais généraux que read). La modification des mappages de mémoire virtuelle est une opération assez coûteuse sur certains processeurs pour les mêmes raisons que la commutation entre différents processus est coûteuse.
  • Le système IO peut déjà utiliser le cache disque, donc si vous lisez un fichier, vous allez frapper le cache ou le manquer, quelle que soit la méthode que vous utilisez.

cependant,

  • Les cartes mémoire sont généralement plus rapides pour un accès aléatoire, en particulier si vos modèles d'accès sont rares et imprévisibles.
  • Les cartes mémoire vous permettent de continuer à utiliser les pages du cache jusqu'à ce que vous ayez terminé. Cela signifie que si vous utilisez beaucoup un fichier pendant une longue période, puis fermez-le et rouvrez-le, les pages seront toujours mises en cache. Avec read, votre fichier a peut-être été vidé du cache il y a longtemps. Cela ne s'applique pas si vous utilisez un fichier et que vous le supprimez immédiatement. (Si vous essayez de mlockpages juste pour les garder dans le cache, vous essayez de déjouer le cache disque et ce genre de sottise aide rarement les performances du système).
  • La lecture directe d'un fichier est très simple et rapide.

La discussion sur mmap / read me rappelle deux autres discussions sur les performances:

  • Certains programmeurs Java ont été choqués de découvrir que les E / S non bloquantes sont souvent plus lentes que les E / S bloquantes, ce qui est parfaitement logique si vous savez que les E / S non bloquantes nécessitent plus d'appels système.

  • Certains autres programmeurs de réseau ont été choqués d'apprendre que epollc'est souvent plus lent que poll, ce qui est parfaitement logique si vous savez que la gestion epollnécessite de faire plus d'appels système.

Conclusion: utilisez des cartes mémoire si vous accédez aux données de manière aléatoire, que vous les gardez longtemps, ou si vous savez que vous pouvez les partager avec d'autres processus (ce MAP_SHAREDn'est pas très intéressant s'il n'y a pas de partage réel). Lisez les fichiers normalement si vous accédez aux données de manière séquentielle ou supprimez-les après la lecture. Et si l'une ou l'autre méthode rend votre programme moins complexe, faites -le . Pour de nombreux cas du monde réel, il n'y a pas de moyen sûr de montrer que l'un est plus rapide sans tester votre application réelle et PAS une référence.

(Désolé d'avoir nécrosé cette question, mais je cherchais une réponse et cette question revenait sans cesse en haut des résultats Google.)

Dietrich Epp
la source
1
Gardez à l'esprit que l'utilisation de tout conseil basé sur du matériel et des logiciels des années 2000, sans les tester aujourd'hui serait une approche très suspecte. De plus, bien que de nombreux faits sur mmapvs read()dans ce thread soient toujours vrais comme dans le passé, les performances globales ne peuvent pas vraiment être déterminées en additionnant les avantages et les inconvénients, mais uniquement en testant une configuration matérielle particulière. Par exemple, il est discutable que "Un appel à mmap a plus de surcharge que de lecture" - oui mmapdoit ajouter des mappages à la table de page de processus, mais readdoit copier tous les octets lus du noyau vers l'espace utilisateur.
BeeOnRope
Le résultat est que, sur mon matériel (Intel moderne, vers 2018), mmapla surcharge est inférieure à celle readdes lectures plus grandes que la taille d'une page (4 Kio). Maintenant, il est très vrai que si vous voulez accéder aux données de manière clairsemée et aléatoire, mmapc'est vraiment très bien - mais l'inverse n'est pas nécessaire: mmappeut-être toujours le meilleur pour un accès séquentiel.
BeeOnRope
1
@BeeOnRope: Vous êtes peut-être sceptique quant aux conseils basés sur le matériel et les logiciels des années 2000, mais je suis encore plus sceptique quant aux benchmarks qui ne fournissent pas de méthodologie et de données. Si vous souhaitez faire un cas mmapplus rapide, je m'attendrais à voir au minimum tout l'appareil de test (code source) avec les résultats tabulés et le numéro de modèle du processeur.
Dietrich Epp
@BeeOnRope: gardez également à l'esprit que lorsque vous testez des bits du système de mémoire comme celui-ci, les microbenchmarks peuvent être extrêmement trompeurs car un vidage TLB peut avoir un impact négatif sur les performances du reste de votre programme, et cet impact ne se manifestera pas si vous ne mesurez que le mmap lui-même.
Dietrich Epp
2
@DietrichEpp - oui, je connais bien les effets TLB. Notez que mmapcela ne vide pas le TLB, sauf dans des circonstances inhabituelles (mais munmappourraient). Mes tests incluaient à la fois des microbenchmarks (y compris munmap) et aussi "in application" s'exécutant dans un cas d'utilisation réel. Bien sûr, mon application n'est pas la même que votre application, donc les gens devraient tester localement. Il n'est même pas clair que ce mmapsoit favorisé par un micro-benchmark: read()obtient également un gros coup de pouce puisque le tampon de destination côté utilisateur reste généralement en L1, ce qui peut ne pas se produire dans une application plus grande. Alors oui, "c'est compliqué".
BeeOnRope
47

Le principal coût de performance sera les E / S de disque. "mmap ()" est certainement plus rapide que istream, mais la différence n'est peut-être pas perceptible car les entrées / sorties de disque domineront vos temps d'exécution.

J'ai essayé le fragment de code de Ben Collins (voir ci-dessus / ci-dessous) pour tester son affirmation selon laquelle "mmap () est beaucoup plus rapide" et je n'ai trouvé aucune différence mesurable. Voir mes commentaires sur sa réponse.

Je ne recommanderais certainement pas de mmaping séparément chaque enregistrement à son tour à moins que vos «enregistrements» ne soient énormes - ce serait horriblement lent, nécessitant 2 appels système pour chaque enregistrement et peut-être perdre la page du cache de la mémoire disque .... .

Dans votre cas, je pense que mmap (), istream et les appels open () / read () de bas niveau seront tous à peu près identiques. Je recommanderais mmap () dans ces cas:

  1. Il y a un accès aléatoire (non séquentiel) dans le fichier, ET
  2. le tout tient confortablement dans la mémoire OU il y a une localité de référence dans le fichier afin que certaines pages puissent être mappées et d'autres mappées. De cette façon, le système d'exploitation utilise la RAM disponible pour un maximum d'avantages.
  3. OU si plusieurs processus lisent / travaillent sur le même fichier, alors mmap () est fantastique car les processus partagent tous les mêmes pages physiques.

(btw - J'adore mmap () / MapViewOfFile ()).

Tim Cooper
la source
Bon point à propos de l'accès aléatoire: cela pourrait être l'une des choses qui motivent ma perception.
Ben Collins
1
Je ne dirais pas que le fichier doit tenir confortablement dans la mémoire, uniquement dans l'espace d'adressage. Ainsi, sur les systèmes 64 bits, il ne devrait y avoir aucune raison de ne pas mapper de gros fichiers. Le système d'exploitation sait comment gérer cela; c'est la même logique que celle utilisée pour l'échange, mais dans ce cas, cela ne nécessite pas d'espace d'échange supplémentaire sur le disque.
MvG
@MvG: Comprenez-vous le point sur les E / S disque? Si le fichier tient dans l'espace d'adressage mais pas dans la mémoire et que vous avez un accès aléatoire, vous pourriez avoir tous les accès d'enregistrement nécessitant un déplacement et une recherche de la tête de disque, ou une opération de page SSD, ce qui serait un désastre pour les performances.
Tim Cooper
3
L'aspect des E / S disque doit être indépendant de la méthode d'accès. Si vous avez un accès vraiment aléatoire à des fichiers plus volumineux que la RAM, mmap et seek + read sont fortement liés au disque. Sinon, les deux bénéficieront des caches. Je ne vois pas la taille du fichier par rapport à la taille de la mémoire comme un argument fort dans les deux sens. La taille du fichier par rapport à l'espace d'adressage, d'autre part, est un argument très fort, en particulier pour un accès vraiment aléatoire.
MvG
Ma réponse originale avait et a ce point: "le tout tient confortablement dans la mémoire OU il y a une localité de référence dans le fichier". Le deuxième point concerne donc ce que vous dites.
Tim Cooper
43

mmap est bien plus rapide. Vous pourriez écrire un simple benchmark pour vous le prouver:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
  in.read(data, 0x1000);
  // do something with data
}

contre:

const int file_size=something;
const int page_size=0x1000;
int off=0;
void *data;

int fd = open("filename.bin", O_RDONLY);

while (off < file_size)
{
  data = mmap(NULL, page_size, PROT_READ, 0, fd, off);
  // do stuff with data
  munmap(data, page_size);
  off += page_size;
}

Clairement, je laisse de côté des détails (comme comment déterminer quand vous atteignez la fin du fichier dans le cas où votre fichier n'est pas un multiple de page_size, par exemple), mais cela ne devrait vraiment pas être beaucoup plus compliqué que cela .

Si vous le pouvez, vous pouvez essayer de diviser vos données en plusieurs fichiers qui peuvent être mmap () - édités en totalité plutôt qu'en partie (beaucoup plus simple).

Il y a quelques mois, j'avais une implémentation à moitié cuite d'une classe de flux mmap () à fenêtre coulissante pour boost_iostreams, mais personne ne s'en souciait et je me suis occupé d'autres choses. Malheureusement, j'ai supprimé une archive d'anciens projets inachevés il y a quelques semaines, et c'était l'une des victimes :-(

Mise à jour : je devrais également ajouter la mise en garde que ce benchmark serait assez différent dans Windows car Microsoft a implémenté un cache de fichiers astucieux qui fait la plupart de ce que vous feriez avec mmap en premier lieu. Par exemple, pour les fichiers fréquemment consultés, vous pouvez simplement faire std :: ifstream.read () et ce serait aussi rapide que mmap, car le cache de fichiers aurait déjà fait un mappage mémoire pour vous, et il est transparent.

Dernière mise à jour : Écoutez, les gens: sur de nombreuses combinaisons de plates-formes différentes de systèmes d'exploitation et de bibliothèques standard, de disques et de hiérarchies de mémoire, je ne peux pas dire avec certitude que l'appel système mmap, considéré comme une boîte noire, sera toujours toujours beaucoup plus rapide que read. Ce n'était pas exactement mon intention, même si mes paroles pouvaient être interprétées de cette façon. En fin de compte, mon point était que les E / S mappées en mémoire sont généralement plus rapides que les E / S basées sur octets; c'est toujours vrai . Si vous constatez expérimentalement qu'il n'y a pas de différence entre les deux, alors la seule explication qui me semble raisonnable est que votre plate-forme implémente le mappage mémoire sous les couvertures d'une manière qui est avantageuse pour la performance des appels àread. La seule façon d'être absolument certain que vous utilisez des E / S mappées en mémoire de manière portable est d'utiliser mmap. Si vous ne vous souciez pas de la portabilité et que vous pouvez vous fier aux caractéristiques particulières de vos plates-formes cibles, l'utilisation readpeut être appropriée sans sacrifier de manière mesurable les performances.

Modifier pour nettoyer la liste de réponses: @jbl:

la fenêtre coulissante mmap semble intéressante. Pouvez-vous en dire un peu plus?

Bien sûr - J'écrivais une bibliothèque C ++ pour Git (une libgit ++, si vous voulez), et j'ai rencontré un problème similaire à celui-ci: je devais être capable d'ouvrir de gros (très gros) fichiers et de ne pas avoir les performances comme un chien total (comme ce serait avec std::fstream).

Boost::Iostreamsa déjà une source mapped_file, mais le problème était qu'il envoyait un mmapping à des fichiers entiers, ce qui vous limite à 2 ^ (taille des mots). Sur les machines 32 bits, 4 Go ne sont pas assez grands. Il n'est pas déraisonnable de s'attendre à avoir des .packfichiers dans Git qui deviennent beaucoup plus volumineux que cela, j'ai donc eu besoin de lire le fichier en morceaux sans recourir à des entrées / sorties de fichiers régulières. Sous les couvertures de Boost::Iostreams, j'ai implémenté une Source, qui est plus ou moins une autre vision de l'interaction entre std::streambufet std::istream. Vous pouvez également essayer une approche similaire en héritant juste std::filebufen mapped_filebufet de même, héritant std::fstreamen a mapped_fstream. C'est l'interaction entre les deux qui est difficile à obtenir. Boost::Iostreams a une partie du travail fait pour vous, et il fournit également des crochets pour les filtres et les chaînes, donc j'ai pensé qu'il serait plus utile de l'implémenter de cette façon.

Ben Collins
la source
3
RE: cache de fichiers mmaped sous Windows. Exactement: lorsque la mise en mémoire tampon de fichiers est activée, la mémoire du noyau mappe le fichier que vous lisez en interne, lit dans ce tampon et le copie à nouveau dans votre processus. C'est comme si votre mémoire l'avait mappé vous-même, sauf avec une étape de copie supplémentaire.
Chris Smith
6
Je ne suis pas d'accord avec une réponse acceptée, mais je crois que cette réponse est fausse. J'ai suivi votre suggestion et essayé votre code, sur une machine Linux 64 bits, et mmap () n'était pas plus rapide que l'implémentation STL. De plus, en théorie, je ne m'attendrais pas à ce que «mmap ()» soit plus rapide (ou plus lent).
Tim Cooper
3
@Tim Cooper: vous trouverez peut-être ce fil ( markmail.org/message/… ) intéressant. Notez les deux choses: mmap n'est pas correctement optimisé sous Linux, et il faut également utiliser madvise dans leur test pour obtenir les meilleurs résultats.
Ben Collins
9
Cher Ben: J'ai lu ce lien. Si 'mmap ()' n'est pas plus rapide sous Linux et MapViewOfFile () n'est pas plus rapide sous Windows, pouvez-vous affirmer que "mmap est bien plus rapide"? De plus, pour des raisons théoriques, je pense que mmap () n'est pas plus rapide pour les lectures séquentielles - avez-vous une explication du contraire?
Tim Cooper
11
Ben, pourquoi se soucier mmap()du fichier une page à la fois? Si a size_test suffisamment volumineux pour contenir la taille du fichier (très probablement sur les systèmes 64 bits), alors juste mmap()le fichier entier en un seul appel.
Steve Emmerson
41

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 , mmappeut sembler un peu comme la magie :

  1. 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.
  2. mmap ne nécessite pas de copie des données du fichier du noyau vers l'espace utilisateur.
  3. mmapvous 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 mmapvs read(2)(qui est vraiment l'appel système comparable au niveau du système d'exploitation pour la lecture de blocs ) est que mmapvous 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 mmapcontient 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à mmappour 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' mmapapproche, 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é mmapqu'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 mallocutiliser un seul petit tampon dans l'espace utilisateur et le réutiliser à plusieurs reprises pour tous vos readappels. 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' mmapapproche 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' mmapapproche 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_POPULATEimplé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_userperformances 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 mmapcas sans MAP_POPULATE.
  • Ajout de copy_to_userméthodes rapides dans arch/x86/lib/copy_user_64.S, par exemple, en utilisant REP MOVQquand 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 mmapmé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_POPULATEet 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 mmapet readappels, 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 madviseou des fadviseappels (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 mmapdans des fenêtres de plus petite taille, par exemple 100 Mo.

4 En fait, il s'avère que l' MAP_POPULATEapproche (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.

BeeOnRope
la source
4
Merci d'avoir fourni une réponse plus nuancée à cette question complexe. Il semble évident pour la plupart des gens que mmap est plus rapide, alors qu'en réalité ce n'est souvent pas le cas. Dans mes expériences, accéder aléatoirement à une grande base de données de 100 Go avec un index en mémoire s'est avéré plus rapide avec pread (), même si je mettais un tampon pour chacun des millions d'accès. Et il semble que beaucoup de gens de l'industrie aient observé la même chose .
Caetano Sauer
5
Ouais, cela dépend beaucoup du scénario. Si vos lectures sont suffisamment petites et avec le temps, vous avez tendance à lire à plusieurs reprises les mêmes octets, mmapcela aura un avantage insurmontable car cela évite la surcharge fixe des appels du noyau. D'autre part, mmapaugmente également la pression TLB et ralentit en fait la phase de "préchauffage" où les octets sont lus pour la première fois dans le processus en cours (bien qu'ils soient toujours dans la page de page), car cela peut le faire plus de travail que read, par exemple, pour "contourner les défauts" des pages adjacentes ... et pour les mêmes applications, "réchauffer" est tout ce qui compte! @CaetanoSauer
BeeOnRope
Je pense que là où vous dites "... mais 25 milliards de défauts de page ne seront toujours pas super rapides ..." il devrait se lire "... mais 25 millions de défauts de page ne seront toujours pas très rapides ..." . Je ne suis pas positif à 100%, c'est pourquoi je n'édite pas directement.
Ton van den Heuvel
7

Je suis désolé que Ben Collins ait perdu le code source de sa fenêtre coulissante mmap. Ce serait bien d'avoir dans Boost.

Oui, le mappage du fichier est beaucoup plus rapide. Vous utilisez essentiellement le sous-système de mémoire virtuelle du système d'exploitation pour associer la mémoire au disque et vice versa. Pensez-y de cette façon: si les développeurs du noyau du système d'exploitation pouvaient le rendre plus rapide, ils le feraient. Parce que cela accélère à peu près tout: bases de données, temps de démarrage, temps de chargement du programme, et cetera.

L'approche de la fenêtre coulissante n'est vraiment pas si difficile car plusieurs pages contingentes peuvent être mappées à la fois. Ainsi, la taille de l'enregistrement n'a pas d'importance tant que le plus grand de n'importe quel enregistrement tient dans la mémoire. L'important est de gérer la comptabilité.

Si un enregistrement ne commence pas sur une limite getpagesize (), votre mappage doit commencer sur la page précédente. La longueur de la région mappée s'étend du premier octet de l'enregistrement (arrondi si nécessaire au multiple inférieur de getpagesize ()) jusqu'au dernier octet de l'enregistrement (arrondi au multiple supérieur de getpagesize ()). Lorsque vous avez terminé de traiter un enregistrement, vous pouvez le démapper () et passer au suivant.

Tout cela fonctionne très bien sous Windows également en utilisant CreateFileMapping () et MapViewOfFile () (et GetSystemInfo () pour obtenir SYSTEM_INFO.dwAllocationGranularity --- pas SYSTEM_INFO.dwPageSize).

mlbrock
la source
Je viens de googler et j'ai trouvé ce petit extrait de dwAllocationGranularity - j'utilisais dwPageSize et tout se cassait. Merci!
wickedchicken
4

mmap devrait être plus rapide, mais je ne sais pas combien. Cela dépend beaucoup de votre code. Si vous utilisez mmap, il est préférable de mmapper tout le fichier en une seule fois, cela vous facilitera la vie. Un problème potentiel est que si votre fichier est supérieur à 4 Go (ou en pratique, la limite est inférieure, souvent 2 Go), vous aurez besoin d'une architecture 64 bits. Donc, si vous utilisez un environnement 32, vous ne voudrez probablement pas l'utiliser.

Cela dit, il existe peut-être un meilleur moyen d'améliorer les performances. Vous avez dit que le fichier d'entrée est analysé plusieurs fois , si vous pouvez le lire en un seul passage et en finir avec lui, cela pourrait potentiellement être beaucoup plus rapide.

Léon Timmermans
la source
3

Vous devriez peut-être pré-traiter les fichiers, de sorte que chaque enregistrement soit dans un fichier séparé (ou au moins que chaque fichier ait une taille mmapable).

Pourriez-vous également effectuer toutes les étapes de traitement pour chaque enregistrement, avant de passer au suivant? Peut-être que cela éviterait une partie des frais généraux d'E / S?

Douglas Leeder
la source
3

Je conviens que les E / S de fichier mmap'd seront plus rapides, mais pendant que vous comparez le code, le contre-exemple ne devrait-il pas être quelque peu optimisé?

Ben Collins a écrit:

char data[0x1000];
std::ifstream in("file.bin");

while (in)
{
    in.read(data, 0x1000);
    // do something with data 
}

Je suggérerais également d'essayer:

char data[0x1000];
std::ifstream iifle( "file.bin");
std::istream  in( ifile.rdbuf() );

while( in )
{
    in.read( data, 0x1000);
    // do something with data
}

Et au-delà de cela, vous pouvez également essayer de faire en sorte que la taille du tampon soit de la même taille qu'une page de mémoire virtuelle, au cas où 0x1000 ne serait pas la taille d'une page de mémoire virtuelle sur votre machine ... IMHO mmap'd file I / O still gagne, mais cela devrait rapprocher les choses.

paxos1977
la source
2

À mon avis, utiliser mmap () "juste" soulage le développeur d'avoir à écrire son propre code de mise en cache. Dans un simple cas de "lecture rapide du fichier une fois", cela ne sera pas difficile (bien que, comme le souligne mlbrock, vous enregistrez toujours la copie mémoire dans l'espace de processus), mais si vous faites des allers-retours dans le fichier ou en sautant des bits et ainsi de suite, je pense que les développeurs du noyau ont probablement fait un meilleur travail d'implémentation de la mise en cache que moi ...

Mike
la source
1
Vous pouvez très probablement faire un meilleur travail de mise en cache des données spécifiques à votre application que le noyau, qui fonctionne sur des morceaux de la taille d'une page de manière très aveugle (par exemple, il n'utilise qu'un simple schéma pseudo-LRU pour décider des pages à expulser. ) - alors que vous en savez peut-être beaucoup sur la granularité de mise en cache appropriée et que vous avez également une bonne idée des futurs modèles d'accès. Le véritable avantage de la mmapmise en cache est que vous réutilisez simplement le cache de page existant qui sera déjà là, vous obtenez donc cette mémoire gratuitement, et elle peut également être partagée entre les processus.
BeeOnRope
2

Je me souviens avoir mappé un énorme fichier contenant une structure arborescente dans la mémoire il y a des années. J'ai été étonné par la vitesse par rapport à la désérialisation normale qui implique beaucoup de travail en mémoire, comme l'allocation de nœuds d'arbre et la définition de pointeurs. Donc, en fait, je comparais un seul appel à mmap (ou son homologue sous Windows) à de nombreux appels (BEAUCOUP) aux appels d'opérateurs et de constructeurs. Pour ce type de tâche, mmap est imbattable par rapport à la désérialisation. Bien sûr, il faut se pencher sur le pointeur relocalisable des boosts pour cela.


la source
Cela ressemble plus à une recette pour un désastre. Que faites-vous si la disposition de l'objet change? Si vous avez des fonctions virtuelles, tous les pointeurs vftbl seront probablement erronés. Comment contrôlez-vous où le fichier est mappé? Vous pouvez lui donner une adresse, mais ce n'est qu'un indice et le noyau peut choisir une autre adresse de base.
Jens
Cela fonctionne parfaitement lorsque vous avez une disposition d'arbre stable et clairement définie. Ensuite, vous pouvez tout transtyper dans vos structures pertinentes et suivre les pointeurs de fichier internes en ajoutant un décalage de "mmap start address" à chaque fois. Ceci est très similaire aux systèmes de fichiers utilisant des inodes et des arborescences de répertoires
Mike76
1

Cela semble être un bon cas d'utilisation pour le multi-threading ... Je pense que vous pourriez assez facilement configurer un thread pour qu'il lise des données pendant que les autres les traitent. Cela peut être un moyen d'augmenter considérablement la performance perçue. Juste une pensée.

Pat Notz
la source
Oui. J'y ai réfléchi et je vais probablement l'essayer dans une version ultérieure. La seule réserve que j'ai est que le traitement est beaucoup plus court que la latence d'E / S, donc il peut ne pas y avoir beaucoup d'avantages.
jbl
1

Je pense que la meilleure chose à propos de mmap est le potentiel de lecture asynchrone avec:

    addr1 = NULL;
    while( size_left > 0 ) {
        r = min(MMAP_SIZE, size_left);
        addr2 = mmap(NULL, r,
            PROT_READ, MAP_FLAGS,
            0, pos);
        if (addr1 != NULL)
        {
            /* process mmap from prev cycle */
            feed_data(ctx, addr1, MMAP_SIZE);
            munmap(addr1, MMAP_SIZE);
        }
        addr1 = addr2;
        size_left -= r;
        pos += r;
    }
    feed_data(ctx, addr1, r);
    munmap(addr1, r);

Le problème est que je ne trouve pas le bon MAP_FLAGS pour donner un indice que cette mémoire doit être synchronisée à partir du fichier dès que possible. J'espère que MAP_POPULATE donne le bon indice pour mmap (c'est-à-dire qu'il n'essaiera pas de charger tout le contenu avant le retour de l'appel, mais le fera en async. Avec feed_data). Au moins, il donne de meilleurs résultats avec cet indicateur, même si le manuel déclare qu'il ne fait rien sans MAP_PRIVATE depuis 2.6.23.

seulement
la source
2
Vous voulez posix_madviseavec leWILLNEED drapeau pour les conseils paresseux à préremplir.
ShadowRanger
@ShadowRanger, cela semble raisonnable. Bien que je mette à jour la page de manuel pour indiquer clairement qu'il posix_madvises'agit d'un appel asynchrone. Ce serait également une bonne référence mlockpour ceux qui veulent attendre que toute la région de la mémoire devienne disponible sans défauts de page.
ony