Utilisation de MemoryMappedFile et MemoryMappedViewAccessor pour l'accès aux fichiers en mémoire

Les fichiers mappés en mémoire (Memory-mapped files) constituent une technique puissante pour interagir avec des fichiers de manière performante, en les intégrant direcetment dans l'espace d'adressage virtuel d'un processus. Le .NET Framework, via les classes System.IO.MemoryMappedFiles.MemoryMappedFile et MemoryMappedViewAccessor, offre des outils robustes pour exploiter cette fonctionnalité.

MemoryMappedFile : La fondation

La classe MemoryMappedFile permet de créer et de gérer des fichiers mappés en mémoire. Contrairement aux approches traditionnelles de lecture/écriture de fichiers qui impliquent des transferts de données explicites entre le noyau et l'espace utilisateur, le mappage en mémoire permet un accès direct aux données du fichier comme s'il s'agissait d'une zone de mémoire vive.

Avantages clés :

  • Performance accrue : En évitant les opérations de disque répétées et les copies de tampons, le mappage en mémoire accélère considérablement les accès en lecture et en écriture, particulièrement avantageux pour les fichiers volumineux.
  • Communication Inter-Processus (IPC) : Plusieurs processus peuvent partager le même fichier mappé en mémoire, facilitant ainsi l'échange de données et la synchronisation entre eux.
  • Gestion optimisée de la mémoire : Le système d'exploitation gère le déplacement des données entre la mémoire vive et le disque selon les besoins, assurant une utilisation efficace des ressources.

Cas d'usage typiques :

  • Traitement de fichiers de grande taille (bases de données, images).
  • Partage de données entre applications indépendantes.
  • Systèmes nécessitant un accès rapide et réactif aux données.

MemoryMappedViewAccessor : L'accès précis

Pour interagir avec le contenu d'un MemoryMappedFile, la classe MemoryMappedViewAccessor est essentielle. Elle offre des capacités d'accès aléatoire aux données, permettant de lire et d'écrire des types de données variés à des positions spécifiques, un peu comme on manipulerait un tableau.

Fonctionnalités principales :

  • Lecture/Écriture de types primitifs (antiers, flottants) et de tableaux.
  • Accès aléatoire à n'importe quelle partie du fichier mappé.

Propriétés utiles :

  • Capacity : Taille de la vue mappée en mémoire.
  • IsClosed / IsDisposed : Indiquent l'état de l'accesseur.

Méthodes courantes :

  • Read<t>(long position)</t> et Write(long position, T value) : Pour lire/écrire des valeurs d'un type spécifique T à une position donnée.
  • ReadArray<t>(long position, T[] array, int offset, int count)</t> et WriteArray<t>(long position, T[] array, int offset, int count)</t> : Pour transférer des blocs de données depuis/vers des tableaux.
  • Flush() : Force l'écriture des modifications en attente vers le stockage sous-jacent.

Exemples d'utilisation

Lecture et écriture de données binaires de base

Cet exemple montre comment créer un fichier mappé en mémoire, y écrire un entier et un tableau d'entiers, puis les relire.


using System;
using System.IO;
using System.IO.MemoryMappedFiles;

public class BasicFileAccess
{
   public static void PerformBasicOperations(string filePath = "data_binary.bin", long fileSize = 1024)
   {
       // Crée ou ouvre un fichier mappé en mémoire d'une taille spécifiée.
       using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Create, null, fileSize))
       {
           // Crée un accesseur pour lire et écrire dans le fichier mappé.
           using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
           {
               // Écriture d'un entier.
               int initialValue = 98765;
               long writePosition = 0;
               accessor.Write(writePosition, initialValue);
               Console.WriteLine($"Écrit la valeur entière : {initialValue} à la position {writePosition}");

               // Lecture de l'entier précédemment écrit.
               int readIntValue = accessor.ReadInt32(writePosition);
               Console.WriteLine($"Lu la valeur entière : {readIntValue} depuis la position {writePosition}");

               // Écriture d'un tableau d'entiers.
               int[] numbersToWrite = { 10, 20, 30, 40, 50 };
               long arrayPosition = 4; // Décalage pour laisser de la place à l'entier précédent.
               accessor.WriteArray(arrayPosition, numbersToWrite, 0, numbersToWrite.Length);
               Console.WriteLine($"Écrit un tableau d'entiers ({numbersToWrite.Length} éléments) à la position {arrayPosition}");

               // Lecture du tableau d'entiers.
               int[] readNumbers = new int[numbersToWrite.Length];
               accessor.ReadArray(arrayPosition, readNumbers, 0, readNumbers.Length);
               Console.Write($"Lu le tableau d'entiers depuis la position {arrayPosition} : ");
               foreach (int num in readNumbers)
               {
                   Console.Write($"{num} ");
               }
               Console.WriteLine();
           }
       }
   }
}
   

Gestion des chaînes de caractères

Pour manipuler des chaînes, il faut les convertir en tableaux d'octets avant de les écrire, puis les reconvertir lors de la lecture. Il est courant de stocker la longueur de la chaîne juste avant les données pour faciliter la lecture.


using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Text;

public class StringDataHandler
{
   public static void HandleStringData(string filePath = "string_data.bin", long fileSize = 1024)
   {
       using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Create, null, fileSize))
       {
           using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
           {
               string message = "Exemple de chaîne pour le mappage mémoire.";
               byte[] messageBytes = Encoding.UTF8.GetBytes(message);

               // Stocke la longueur de la chaîne à la position 0.
               accessor.Write(0, messageBytes.Length);

               // Écrit les octets de la chaîne à partir de la position 4 (après la longueur).
               accessor.WriteArray(4, messageBytes, 0, messageBytes.Length);
               Console.WriteLine($"Écrit la chaîne '{message}' et sa longueur ({messageBytes.Length} octets).");

               // Lit la longueur de la chaîne.
               int length = accessor.ReadInt32(0);

               // Lit les octets de la chaîne.
               byte[] readBytes = new byte[length];
               accessor.ReadArray(4, readBytes, 0, length);

               // Convertit les octets en chaîne.
               string readMessage = Encoding.UTF8.GetString(readBytes);
               Console.WriteLine($"Lu la chaîne : '{readMessage}'");
           }
       }
   }
}
   

Manipulation d'objets personnalisés

Pour les objets, une étape de sérialisation est nécessaire avant l'écriture et une désérialisation après la lecture. Le BinaryFormatter est une option courante pour cela.


using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Runtime.Serialization.Formatters.Binary;

[Serializable]
public class UserProfile
{
   public string Username { get; set; }
   public int Level { get; set; }
}

public class ObjectDataHandler
{
   public static void HandleObjectData(string filePath = "object_data.bin", long fileSize = 1024)
   {
       using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Create, null, fileSize))
       {
           using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
           {
               UserProfile profileToWrite = new UserProfile { Username = "Alice", Level = 5 };
               BinaryFormatter formatter = new BinaryFormatter();

               // Sérialisation de l'objet en flux d'octets.
               using (MemoryStream ms = new MemoryStream())
               {
                   formatter.Serialize(ms, profileToWrite);
                   byte[] objectBytes = ms.ToArray();

                   // Stocke la taille de l'objet sérialisé.
                   accessor.Write(0, objectBytes.Length);
                   // Écrit les octets de l'objet.
                   accessor.WriteArray(4, objectBytes, 0, objectBytes.Length);
                   Console.WriteLine($"Écrit un objet UserProfile (taille: {objectBytes.Length} octets).");
               }

               // Lecture de la taille de l'objet.
               int length = accessor.ReadInt32(0);

               // Lecture des octets de l'objet.
               byte[] readBytes = new byte[length];
               accessor.ReadArray(4, readBytes, 0, length);

               // Désérialisation de l'objet.
               using (MemoryStream ms = new MemoryStream(readBytes))
               {
                   UserProfile readProfile = (UserProfile)formatter.Deserialize(ms);
                   Console.WriteLine($"Lu l'objet UserProfile : Nom={readProfile.Username}, Niveau={readProfile.Level}");
               }
           }
       }
   }
}
   

Accès direct aux types numériques

MemoryMappedViewAccessor propose des méthodes dédiées pour les types numériques courants, simplifiant leur gestion sans nécesstier de conversion manuelle.


using System;
using System.IO;
using System.IO.MemoryMappedFiles;

public class NumericDataHandler
{
   public static void HandleNumericData(string filePath = "numeric_data.bin", long fileSize = 1024)
   {
       using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(filePath, FileMode.Create, null, fileSize))
       {
           using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
           {
               int sampleInt = 54321;
               long sampleLong = 9876543210L;
               float sampleFloat = 9.87f;
               double sampleDouble = 123.456789;

               // Écriture des valeurs numériques à des positions spécifiques.
               accessor.Write(0, sampleInt);     // Écrit un Int32 à l'offset 0.
               accessor.Write(4, sampleLong);    // Écrit un Int64 à l'offset 4 (prend 8 octets).
               accessor.Write(12, sampleFloat);  // Écrit un Single (float) à l'offset 12 (prend 4 octets).
               accessor.Write(16, sampleDouble); // Écrit un Double à l'offset 16 (prend 8 octets).

               Console.WriteLine($"Écrit les valeurs numériques.");

               // Lecture des valeurs numériques.
               int readInt = accessor.ReadInt32(0);
               long readLong = accessor.ReadInt64(4);
               float readFloat = accessor.ReadSingle(12);
               double readDouble = accessor.ReadDouble(16);

               Console.WriteLine($"Lu : Entier={readInt}, Long={readLong}, Flottant={readFloat}, Double={readDouble}");
           }
       }
   }
}
   

Pour les types numériques fondamentaux, les méthodes dédiées de MemoryMappedViewAccessor simplifient l'écriture et la lecture. Pour les structures de données plus complexes comme les chaînes ou les objets, une conversion préalable en tableaux d'octets (via encodage ou sérialisation) est nécessaire.

Étiquettes: MemoryMappedFile MemoryMappedViewAccessor IO fichier performance

Publié le 4 juillet à 02h55