Python mmap : Améliorer les entrées et les sorties avec du mapping en mémoire

Pour suivre nos dernières actualités n'oubliez pas de vous abonner :
Publicité :

Le zen of python dispose de beaucoup de sagesse.

Dans toute cette sagesse, on y retrouve une idée plutôt intéressante.

Il ne devrait y avoir une, de préférence une, sale manière qui coule de source pour le faire.

Alors qu'il existera en python toujours plusieurs manières de résoudre un problème.

Il y a plusieurs manières de lire un fichier, notamment avec le module peu utilisé mmap

Le module mmap donne la possibilité de maper en mémoire les entrées et les sorties I/O d' un fichier.

Ce qui permet de prendre avantage des fonctionnalités de lecture bas niveau du système d'exploitation, en faisant croire que le fichier n'est qu'une simple chaîne de caractère ou un tableau (array).

Ça peut procurer une amélioration de performances énorme dans du code qui utilise la gestion d'entrée et de sortie.

Dans ce tutoriel vous allez apprendre :

Dans ce tutoriel vous allez apprendre:

  • Quel type de mémoires existent dans un ordinateur
  • Quels problèmes peut-on résoudre avec mmap
  • Comment faire du mapping en mémoire pour lire de grands fichiers rapidement
  • Comment changer une partie du fichier sans avoir besoin de réécrire tout le fichier.
  • Comment utiliser mmap pour partager des informations entre différents processus


Comprendre la mémorisation des ordinateurs

Le mappage de mémoire est une technique qui utilise des API de système d'exploitation de niveau inférieur pour charger un fichier directement dans la mémoire de l'ordinateur.


Elle peut améliorer considérablement les performances d'E/S des fichiers dans votre programme. 

Pour mieux comprendre comment le mappage de mémoire améliore les performances ainsi que comment et quand vous pouvez utiliser le module mmap pour profiter de ces avantages, il est utile d'en savoir un peu plus sur le fonctionnement de la mémoire de l'ordinateur.

La mémoire des ordinateurs est un sujet vaste et complexe, mais ce tutoriel se concentre uniquement sur ce que vous devez savoir pour utiliser efficacement le module mmap. Dans le cadre de ce tutoriel, le terme mémoire fait référence à la mémoire vive, ou RAM.

Il existe plusieurs types de mémoire informatique :

  1. Physique
  2. Virtuelle
  3. Partagée

Chaque type de mémoire peut entrer en jeu lorsque vous utilisez le mapping de mémoire, alors passons en revue chacun d'entre eux à partir du niveau le plus élevé.

La mémoire physique

La mémoire physique est le type de mémoire le moins compliqué à comprendre car elle fait souvent partie du marketing associé à votre ordinateur. (Vous vous souvenez peut-être que lorsque vous avez acheté votre ordinateur, il annonçait quelque chose comme 8 gigaoctets de RAM). La mémoire physique se présente généralement sous la forme de barettes connectées à la carte mère de votre ordinateur.

La mémoire physique est la quantité de mémoire volatile disponible pour vos programmes en cours d'exécution. La mémoire physique ne doit pas être confondue avec le stockage, tel que votre disque dur ou votre disque à semi-conducteurs.

Mémoire virtuelle

La mémoire virtuelle est un moyen de gérer la mémoire. 

Le système d'exploitation utilise la mémoire virtuelle pour donner l'impression que vous avez plus de mémoire que vous n'en avez réellement, ce qui vous permet de moins vous soucier de la quantité de mémoire disponible pour vos programmes à un moment donné. 

En coulisse, votre système d'exploitation utilise des parties de votre stockage non volatile, comme votre disque dur, pour simuler de la RAM supplémentaire.

Pour ce faire, votre système d'exploitation doit maintenir une correspondance entre la mémoire physique et la mémoire virtuelle. 

Chaque système d'exploitation utilise son propre algorithme sophistiqué pour faire correspondre les adresses de la mémoire virtuelle aux adresses physiques à l'aide d'une structure de données appelée table des pages.


Heureusement, la plupart de ces complications sont cachées dans vos programmes. Vous n'avez pas besoin de comprendre les tables de pages ou le mappage logique-physique pour écrire du code prenant en compte les entrées et sorties en Python. Cependant, le fait de connaître un peu la mémoire vous permet de mieux comprendre ce dont l'ordinateur et les bibliothèques s'occupent pour vous.

mmap utilise la mémoire virtuelle pour faire croire que vous avez chargé un très gros fichier en mémoire, même si le contenu du fichier est trop gros pour tenir dans votre mémoire physique.

Mémoire partagée

La mémoire partagée est une autre technique proposée par votre système d'exploitation qui permet à plusieurs programmes d'accéder simultanément aux mêmes données. La mémoire partagée peut être un moyen très efficace de gérer les données dans un programme qui utilise la concurrence.

mmap de Python utilise la mémoire partagée pour répartir efficacement de grandes quantités de données entre plusieurs processus, threads et tâches Python qui se déroulent simultanément.

Creusons un peu plus sur la gestion I/O des fichiers

Maintenant que vous avez une vue d'ensemble des différents types de mémoire, il est temps de comprendre ce qu'est le mapping de mémoire et quels problèmes il résout. Le mapping de mémoire est une autre façon d'effectuer des E/S de fichiers qui peut se traduire par de meilleures performances et une meilleure allocation de la mémoire.

Pour bien comprendre ce que fait le mapping de la mémoire, il est utile de considérer les E/S de fichiers ordinaires dans une perspective de plus bas niveau. Beaucoup de choses se passent en coulisses lors de la lecture d'un fichier :

  1. Transfert du contrôle au noyau ou au code central du système d'exploitation à l'aide d'appels système.
  2. Interaction avec le disque physique où réside le fichier.
  3. Copie des données dans différents tampons entre l'espace utilisateur et l'espace noyau.

Considérons le code suivant, qui exécute des entrées/sorties de fichiers Python ordinaires :

def regular_io(filename):

    with open(filename, mode="r", encoding="utf8") as file_obj:

        text = file_obj.read()

        print(text)


Ce code lit le fichier entier dans la mémoire physique, s'il y en a assez de disponible au moment de l'exécution, et l'imprime à l'écran.

Ce type d'entrée/sortie de fichier est quelque chose que vous avez peut-être appris au début de votre apprentissage de Python. 

Le code n'est pas très dense ou compliqué. Cependant, ce qui se passe sous le capuchon des appels de fonction comme read() est très compliqué. 

N'oubliez pas que Python est un langage de programmation de haut niveau, et qu'une grande partie de la complexité peut être cachée au programmeur.


Appels système

En réalité, l'appel à read() indique au système d'exploitation qu'il doit effectuer un grand nombre de tâches sophistiquées. 

Heureusement, les systèmes d'exploitation offrent un moyen d'abstraire les détails spécifiques de chaque matériel de vos programmes à l'aide d'appels système.

Chaque système d'exploitation implémente cette fonctionnalité différemment, mais au minimum, read() doit effectuer plusieurs appels système pour récupérer les données du fichier.

Tout accès au matériel physique doit se faire dans un environnement protégé appelé espace noyau.

Les appels système sont l'API que le système d'exploitation fournit pour permettre à votre programme de passer de l'espace utilisateur à l'espace noyau, où sont gérés les détails de bas niveau du matériel physique.

Dans le cas de read(), plusieurs appels système sont nécessaires pour que le système d'exploitation interagisse avec le périphérique de stockage physique et renvoie les données.

Là encore, il n'est pas nécessaire de connaître en détail les appels système et l'architecture des ordinateurs pour comprendre le mapping de la mémoire.

La chose la plus importante à retenir est que les appels système sont relativement coûteux en termes de calcul. Par conséquent, moins vous ferez d'appels système, plus votre code s'exécutera rapidement.

En plus des appels système, l'appel à read() implique également un grand nombre de copies de données potentiellement inutiles entre plusieurs tampons avant que les données ne reviennent à votre programme.

En général, tout cela se passe si vite que l'on ne le remarque pas. Mais toutes ces couches ajoutent de la latence et peuvent ralentir votre programme. C'est à ce niveau que le mapping de la mémoire entre en jeu.

Optimisations du mapping de la mémoire

Une façon d'éviter cette surcharge est d'utiliser un fichier mappé en mémoire.

Vous pouvez vous représenter le mapping de mémoire comme un processus dans lequel les opérations de lecture et d'écriture sautent plusieurs des couches mentionnées ci-dessus et mappent les données demandées directement dans la mémoire physique.

Une approche entrée et sorties de fichiers mappés en mémoire sacrifie l'utilisation de la mémoire pour la vitesse, ce qui est classiquement appelé le compromis espace-temps.

Cependant, le mapping mémoire ne doit pas nécessairement utiliser plus de mémoire que l'approche conventionnelle.

 Le système d'exploitation est très intelligent. Il chargera paresseusement les données au fur et à mesure qu'elles sont demandées, un peu comme le font les générateurs Python.

En plus, grâce à la mémoire virtuelle, vous pouvez charger un fichier qui est plus grand que votre mémoire physique.

Toutefois, vous ne constaterez pas les énormes améliorations de performances que permet le mappage de la mémoire lorsqu'il n'y a pas assez de mémoire physique pour votre fichier, car le système d'exploitation utilisera un support de stockage physique plus lent, comme un disque dur, pour imiter la mémoire physique (RAM) qui lui fait défaut.

Lecture d'un fichier en mémoire avec mmap

Maintenant que toute cette théorie est écartée, vous vous demandez peut-être : "Comment puis-je utiliser le module mmap de Python pour créer un fichier de mémoire ?

Voici l'équivalent en mémoire du code d'entrée/sortie de fichier que vous avez vu auparavant :

import mmap


def mmap_io(filename):

    with open(filename, mode="r", encoding="utf8") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj:

            text = mmap_obj.read()

            print(text)


Ce code lit un fichier entier en mémoire sous la forme d'une chaîne de caractères et l'imprime à l'écran, tout comme le faisait l'approche précédente avec les E/S de fichiers ordinaires.

En résumé, l'utilisation de mmap est assez similaire à la manière traditionnelle de lire un fichier, à quelques petites modifications près :



  1. Ouvrir le fichier avec open() n'est pas suffisant. Vous devez également utiliser mmap.mmap() pour signaler au système d'exploitation que vous voulez que le fichier soit mappé en RAM.

  2. Vous devez vous assurer que le mode que vous utilisez avec open() est compatible avec mmap.mmap(). Le mode par défaut de open() est la lecture, mais celui de mmap.mmap() est à la fois la lecture et l'écriture. Vous devrez donc être explicite lors de l'ouverture du fichier.

  3. Vous devez effectuer toutes les lectures et écritures en utilisant l'objet mmap au lieu de l'objet fichier standard retourné par open().

Implications en termes de performance

L'approche de mapping de mémoire est légèrement plus compliquée que l'E/S de fichier typique car elle nécessite la création d'un autre objet.

Toutefois, ce petit changement peut se traduire par des gains de performances importants lors de la lecture d'un fichier de quelques mégaoctets seulement. 

Voici une comparaison de la lecture du texte brut du célèbre roman L'histoire de Don Quichotte, qui fait environ 2,4 mégaoctets :

>>> import timeit

>>> timeit.repeat(

...    "regular_io(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import regular_io, filename")

[0.02022400000000002, 0.01988580000000001, 0.020257300000000006]

>>> timeit.repeat(

...    "mmap_io(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import mmap_io, filename")

[0.006156499999999981, 0.004843099999999989, 0.004868600000000001]


Il s'agit de mesurer le temps nécessaire pour lire un fichier entier de 2,4 mégaoctets en utilisant l'E/S de fichier ordinaire et l'E/S de fichier mappé en mémoire.

Comme vous pouvez le constater, l'approche mappée en mémoire prend environ 0,005 seconde contre près de 0,02 seconde pour l'approche normale. Cette amélioration des performances peut être encore plus importante lors de la lecture d'un fichier plus volumineux.

Note: Ces résultats ont été recueillis sous Windows 10 et Python 3.8. Le mapping de la mémoire étant très dépendant des implémentations du système d'exploitation, vos résultats peuvent varier.

L'API fournie par l'objet fichier mmap de Python est très similaire à l'objet fichier traditionnel, à l'exception près d'un super pouvoir supplémentaire : L'objet fichier mmap de Python peut être découpé en slices, tout comme les chaînes de caractères !

Création d’un objet mmap

Il y a quelques subtilités dans la création de l'objet mmap qui valent la peine d'être examinées de plus près :

mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ)


mmap nécessite un descripteur de fichier, qui provient de la méthode fileno() d'un objet fichier ordinaire.

Un descripteur de fichier est un identifiant interne, généralement un nombre entier, que le système d'exploitation utilise pour garder la trace des fichiers ouverts.

Le deuxième argument de mmap est length=0. C'est la longueur en octets de la carte mémoire. 0 est une valeur spéciale indiquant que le système doit créer une carte mémoire suffisamment grande pour contenir le fichier entier

L'argument access indique au système d'exploitation comment vous allez interagir avec la mémoire mappée.

Les options sont ACCESS_READ, ACCESS_WRITE, ACCESS_COPY, et ACCESS_DEFAULT. Elles sont quelque peu similaires aux arguments de mode de la fonction intégrée open() :

  • ACCESS_READ crée une carte de la mémoire en lecture seule.
  • ACCESS_DEFAULT utilise par défaut le mode spécifié dans l'argument optionnel prot, qui est utilisé pour la protection de la mémoire.
  • ACCESS_WRITE et ACCESS_COPY sont les deux modes d'écriture, que vous découvrirez plus loin.

Le descripteur de fichier, la longueur et les arguments d'accès représentent le strict minimum dont vous avez besoin pour créer un fichier mappé en mémoire qui fonctionnera sur des systèmes d'exploitation comme Windows, Linux et macOS.

Le code ci-dessus est multiplateforme, ce qui signifie qu'il lira le fichier par le biais de l'interface de mapping mémoire sur tous les systèmes d'exploitation sans avoir besoin de savoir sur quel système d'exploitation le code s'exécute.

Un autre argument utile est l'offset, qui peut être une technique d'économie de mémoire. Cela indique à mmap de créer une carte mémoire à partir d'un décalage spécifié dans le fichier.

mmap Objects comme chaînes de caractères

Comme nous l'avons mentionné précédemment, le mapping de mémoire charge de manière transparente le contenu du fichier en mémoire sous forme de chaînes de caractères.

Ainsi, une fois que vous avez ouvert le fichier, vous pouvez effectuer un grand nombre d'opérations identiques à celles que vous utilisez avec les chaînes de caractères, comme le découpage en slices :

import mmap


def mmap_io(filename):

    with open(filename, mode="r", encoding="utf8") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj:

            print(mmap_obj[10:20])


Ce code imprime dix caractères de mmap_obj à l'écran et lit également ces dix caractères dans la mémoire physique. Encore une fois, les données sont lues paresseusement.

La tranche n'avance pas la position interne du fichier. Ainsi, si vous appelez read() après une tranche, vous lirez toujours depuis le début du fichier.

Rechercher un fichier en mémoire

En plus du slicing, le module mmap permet d'autres actions liées aux chaînes de caractères, comme l'utilisation de find() et rfind() pour rechercher un texte spécifique dans un fichier. Par exemple, voici deux approches pour trouver la première occurrence de " the " dans un fichier :

import mmap


def regular_io_find(filename):

    with open(filename, mode="r", encoding="utf-8") as file_obj:

        text = file_obj.read()

        print(text.find(" the "))


def mmap_io_find(filename):

    with open(filename, mode="r", encoding="utf-8") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj:

            print(mmap_obj.find(b" the "))


Ces deux fonctions recherchent toutes deux dans un fichier la première occurrence de " the ". La principale différence entre elles est que la première utilise find() sur un objet chaîne de caractères, tandis que la seconde utilise find() sur un objet fichier mappé en mémoire.

Note: mmap opère sur des octets, pas sur des chaînes de caractères.

Voici la différence de performance :

>>> import timeit

>>> timeit.repeat(

...    "regular_io_find(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import regular_io_find, filename")

[0.01919180000000001, 0.01940510000000001, 0.019157700000000027]

>>> timeit.repeat(

...    "mmap_io_find(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import mmap_io_find, filename")

[0.0009397999999999906, 0.0018005999999999855, 0.000826699999999958]


C'est une différence de plusieurs ordres de grandeur ! Encore une fois, vos résultats peuvent varier en fonction de votre système d'exploitation.

Les fichiers à mémoire peuvent également être utilisés directement avec des expressions régulières. Prenons l'exemple suivant qui recherche et imprime tous les mots de cinq lettres :

import re

import mmap


def mmap_io_re(filename):

    five_letter_word = re.compile(rb"\b[a-zA-Z]{5}\b")


    with open(filename, mode="r", encoding="utf-8") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj:

            for word in five_letter_word.findall(mmap_obj):

                print(word)


Ce code lit l'ensemble du fichier et affiche tous les mots contenant exactement cinq lettres. N'oubliez pas que les fichiers mappés en mémoire fonctionnent avec des chaînes d'octets. Les expressions régulières doivent donc également utiliser des chaînes d'octets.

Voici le code équivalent en utilisant l'entrée/sortie de fichier ordinaire :

import re


def regular_io_re(filename):

    five_letter_word = re.compile(r"\b[a-zA-Z]{5}\b")


    with open(filename, mode="r", encoding="utf-8") as file_obj:

        for word in five_letter_word.findall(file_obj.read()):

            print(word)


Ce code imprime également tous les mots de cinq caractères du fichier, mais il utilise le mécanisme traditionnel d'E/S de fichier au lieu des fichiers mappés en mémoire. Comme précédemment, les performances diffèrent entre les deux approches :

>>> import timeit

>>> timeit.repeat(

...    "regular_io_re(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import regular_io_re, filename")

[0.10474110000000003, 0.10358619999999996, 0.10347820000000002]

>>> timeit.repeat(

...    "mmap_io_re(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import mmap_io_re, filename")

[0.0740976000000001, 0.07362639999999998, 0.07380980000000004]


L'approche par la mémoire est toujours plus rapide d'un ordre de grandeur.


-------------

Objets mappés en mémoire en tant que fichiers

Un fichier mappé en mémoire est à la fois une chaîne de caractères et un fichier, et mmap vous permet également d'effectuer des opérations courantes sur les fichiers comme seek(), tell() et readline(). Ces fonctions fonctionnent exactement de la même manière que leurs homologues normaux de type fichier-objet.

Par exemple, voici comment chercher un emplacement particulier dans un fichier et ensuite effectuer une recherche sur un mot :

import mmap


def mmap_io_find_and_seek(filename):

    with open(filename, mode="r", encoding="utf-8") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj:

            mmap_obj.seek(10000)

            mmap_obj.find(b" the ")


Ce code va chercher jusqu'à l'emplacement 10000 dans le fichier et ensuite trouver l'emplacement de la première occurrence de " the ".

seek() fonctionne exactement de la même manière sur les fichiers à mémoire partagée que sur les fichiers normaux :

def regular_io_find_and_seek(filename):

    with open(filename, mode="r", encoding="utf-8") as file_obj:

        file_obj.seek(10000)

        text = file_obj.read()

        text.find(" the ")


Le code des deux approches est très similaire. Voyons comment leurs performances se comparent :

>>> import timeit

>>> timeit.repeat(

...    "regular_io_find_and_seek(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import regular_io_find_and_seek, filename")

[0.019396099999999916, 0.01936059999999995, 0.019192100000000045]

>>> timeit.repeat(

...    "mmap_io_find_and_seek(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import mmap_io_find_and_seek, filename")

[0.000925100000000012, 0.000788299999999964, 0.0007854999999999945]


Là encore, après seulement quelques petites modifications du code, votre approche par mappe mémoire est beaucoup plus rapide.

Écriture d'un fichier en mémoire avec mmap de Python

Le mappage de mémoire est surtout utile pour lire des fichiers, mais vous pouvez également l'utiliser pour écrire des fichiers. L'API mmap pour l'écriture de fichiers est très similaire à l'E/S de fichier ordinaire, à quelques différences près.

Voici un exemple d'écriture de texte dans un fichier mappé en mémoire :

import mmap


def mmap_io_write(filename, text):

    with open(filename, mode="w", encoding="utf-8") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_WRITE) as mmap_obj:

            mmap_obj.write(text)


Ce code écrit du texte dans un fichier mappé en mémoire. Cependant, il lève une exception ValueError si le fichier est vide au moment où vous créez l'objet mmap.

Le module mmap de Python ne permet pas de mapper en mémoire un fichier vide. Ceci est raisonnable car, conceptuellement, un fichier vide mappé en mémoire est juste un tampon de mémoire, donc aucun objet de mappage de mémoire n'est nécessaire.

En général, le mappage de mémoire est utilisé en mode lecture ou lecture/écriture. Par exemple, le code suivant montre comment lire rapidement un fichier et n'en modifier qu'une partie :


import mmap


def mmap_io_write(filename):

    with open(filename, mode="r+") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_WRITE) as mmap_obj:

            mmap_obj[10:16] = b"python"

            mmap_obj.flush()


Cette fonction va ouvrir un fichier qui contient déjà au moins seize caractères et changer les caractères 10 à 15 en "python".

Les changements écrits dans mmap_obj sont visibles dans le fichier sur le disque ainsi qu'en mémoire. La documentation officielle de Python recommande de toujours appeler flush() pour garantir que les données sont réécrites sur le disque.


Mode d’écritures

La sémantique de l'opération d'écriture est contrôlée par le paramètre access. Les options du paramètre d'accès permettent de différencier l'écriture de fichiers mappés en mémoire de celle de fichiers ordinaires. Il existe deux options pour contrôler la façon dont les données sont écrites dans un fichier à mémoire partagée :

  1. ACCESS_WRITE spécifie une sémantique d'écriture directe, ce qui signifie que les données seront écrites en mémoire et conservées sur le disque.
  2. ACCESS_COPY n'écrit pas les changements sur le disque, même si flush() est appelé.

En d'autres termes, ACCESS_WRITE écrit à la fois dans la mémoire et dans le fichier, alors que ACCESS_COPY n'écrit que dans la mémoire et pas dans le fichier sous-jacent.

Recherche et remplacement de texte

Les fichiers mappés en mémoire exposent les données sous la forme d'une chaîne d'octets, mais cette chaîne d'octets présente un autre avantage important par rapport à une chaîne ordinaire. 

Les données des fichiers mappés en mémoire sont une chaîne d'octets mutables. Cela signifie qu'il est beaucoup plus simple et efficace d'écrire du code qui recherche et remplace les données dans un fichier :

import mmap

import os

import shutil


def regular_io_find_and_replace(filename):

    with open(filename, "r", encoding="utf-8") as orig_file_obj:

        with open("tmp.txt", "w", encoding="utf-8") as new_file_obj:

            orig_text = orig_file_obj.read()

            new_text = orig_text.replace(" the ", " eht ")

            new_file_obj.write(new_text)


    shutil.copyfile("tmp.txt", filename)

    os.remove("tmp.txt")


def mmap_io_find_and_replace(filename):

    with open(filename, mode="r+", encoding="utf-8") as file_obj:

        with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_WRITE) as mmap_obj:

            orig_text = mmap_obj.read()

            new_text = orig_text.replace(b" the ", b" eht ")

            mmap_obj[:] = new_text

            mmap_obj.flush()


Ces deux fonctions changent le mot " the " en " eht " dans le fichier donné. Comme vous pouvez le constater, l'approche par mappe mémoire est à peu près la même, mais elle ne nécessite pas de garder manuellement la trace d'un fichier temporaire supplémentaire pour effectuer le remplacement sur place.

Dans ce scénario, l'approche par mappage mémoire est en fait légèrement plus lente pour cette longueur de fichier. Ainsi, effectuer une recherche et un remplacement complets sur un fichier mappé en mémoire n'est pas forcément l'approche la plus efficace. Cela dépend probablement de nombreux facteurs, tels que la longueur du fichier, la vitesse de la RAM de votre machine, etc. Il peut également y avoir une mise en cache du système d'exploitation qui fausse les temps. Comme vous pouvez le constater, l'approche IO normale a accéléré à chaque appel.

>>> import timeit

>>> timeit.repeat(

...    "regular_io_find_and_replace(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import regular_io_find_and_replace, filename")

[0.031016973999996367, 0.019185273000005054, 0.019321329999996806]

>>> timeit.repeat(

...    "mmap_io_find_and_replace(filename)",

...    repeat=3,

...    number=1,

...    setup="from __main__ import mmap_io_find_and_replace, filename")

[0.026475408999999672, 0.030173652999998524, 0.029132930999999473]


Dans ce scénario de recherche et de remplacement de base, le mappage de la mémoire permet d'obtenir un code légèrement plus concis, mais pas toujours une amélioration massive de la vitesse. Comme on dit, "votre kilométrage peut varier".

Partage de données entre processus avec mmap de Python

Jusqu'à présent, vous avez utilisé les fichiers de cartes mémoire uniquement pour les données sur le disque. Cependant, vous pouvez également créer des cartes mémoire anonymes qui n'ont pas de stockage physique. Ceci peut être fait en passant -1 comme descripteur de fichier :

import mmap


with mmap.mmap(-1, length=100, access=mmap.ACCESS_WRITE) as mmap_obj:

    mmap_obj[0:100] = b"a" * 100

    print(mmap_obj[0:100])


Cela crée un objet anonyme en mémoire dans la RAM qui contient 100 copies de la lettre "a".

Un objet anonyme mappé en mémoire est essentiellement un tampon d'une taille spécifique, spécifiée par le paramètre length, en mémoire. Le tampon est similaire à io.StringIO ou io.BytesIO de la bibliothèque standard. Cependant, un objet anonyme mappé en mémoire supporte le partage entre plusieurs processus, ce que ni io.StringIO ni io.BytesIO ne permettent.

Cela signifie que vous pouvez utiliser des objets anonymes mappés en mémoire pour échanger des données entre des processus, même si ces derniers ont des mémoires et des piles complètement distinctes. Voici un exemple de création d'un objet anonyme à mappe mémoire pour partager des données qui peuvent être écrites et lues par les deux processus

import mmap


def sharing_with_mmap():

    BUF = mmap.mmap(-1, length=100, access=mmap.ACCESS_WRITE)


    pid = os.fork()

    if pid == 0:

        # Child process

        BUF[0:100] = b"a" * 100

    else:

        time.sleep(2)

        print(BUF[0:100])


Avec ce code, vous créez une mémoire tampon de 100 octets et permettez à cette mémoire tampon d'être lue et écrite par les deux processus. Cette approche peut être utile si vous souhaitez économiser de la mémoire tout en partageant une grande quantité de données entre plusieurs processus.

Le partage de la mémoire avec le mappage de la mémoire présente plusieurs avantages :

  • Les données ne doivent pas être copiées entre les processus.
  • Le système d'exploitation gère la mémoire de manière transparente.
  • Les données n'ont pas besoin d'être mis sous picklelisation entre les processus, ce qui permet d'économiser du temps CPU...

En parlant de pickling, il convient de souligner que mmap est incompatible avec des API de plus haut niveau, plus complètes, comme le module de multitraitement intégré. Le module de multitraitement exige que les données passées entre les processus supportent le protocole pickle, ce que mmap ne fait pas.

Vous pourriez être tenté d'utiliser le multitraitement au lieu de os.fork(), comme dans l'exemple suivant :

from multiprocessing import Process


def modify(buf):

    buf[0:100] = b"xy" * 50


if __name__ == "__main__":

    BUF = mmap.mmap(-1, length=100, access=mmap.ACCESS_WRITE)

    BUF[0:100] = b"a" * 100

    p = Process(target=modify, args=(BUF,))

    p.start()

    p.join()

    print(BUF[0:100])


Ici, vous tentez de créer un nouveau processus et de lui transmettre le tampon mappé en mémoire. Ce code lèvera immédiatement une TypeError car l'objet mmap ne peut pas être pickled, ce qui est nécessaire pour transmettre les données au second processus. Ainsi, pour partager des données à l'aide du mappage de la mémoire, vous devrez vous en tenir à la méthode de plus bas niveau os.fork().

Si vous utilisez Python 3.8 ou une version plus récente, vous pouvez utiliser le nouveau module shared_memory pour partager plus efficacement les données entre les processus Python :

from multiprocessing import Process

from multiprocessing import shared_memory


def modify(buf_name):

    shm = shared_memory.SharedMemory(buf_name)

    shm.buf[0:50] = b"b" * 50

    shm.close()


if __name__ == "__main__":

    shm = shared_memory.SharedMemory(create=True, size=100)


    try:

        shm.buf[0:100] = b"a" * 100

        proc = Process(target=modify, args=(shm.name,))

        proc.start()

        proc.join()

        print(bytes(shm.buf[:100]))

    finally:

        shm.close()

        shm.unlink()


Ce petit programme crée une liste de 100 caractères et modifie les 50 premiers à partir d'un autre processus.

Remarquez que seul le nom du tampon est transmis au second processus. Ensuite, le second processus peut récupérer ce même bloc de mémoire en utilisant le nom unique. Il s'agit d'une caractéristique spéciale du module shared_memory qui est alimenté par mmap. Sous le capot, le module shared_memory utilise l'API propre à chaque système d'exploitation pour créer des cartes mémoire nommées.

Vous connaissez maintenant certains des détails de mise en œuvre sous-jacents de la nouvelle fonctionnalité de mémoire partagée de Python 3.8 ainsi que la façon d'utiliser directement mmap !


Conclusion

Le mappage de mémoire est une approche alternative à l'E/S de fichiers, accessible aux programmes Python par le biais du module mmap. Le mappage de mémoire utilise des API de système d'exploitation de niveau inférieur pour stocker le contenu des fichiers directement dans la mémoire physique. Cette approche se traduit souvent par une amélioration des performances d'E/S car elle évite de nombreux appels système coûteux et réduit les transferts coûteux de tampons de données.

Tu as bien aimé l'article ou tu as mal aux yeux :

Le lien du PDF

Article écrit par :
Mikael Monjour
Data et Automatisation