CodeGym /Cours /C# SELF /Performance et gestion manuelle du buffer

Performance et gestion manuelle du buffer

C# SELF
Niveau 41 , Leçon 3
Disponible

1. Introduction

Pourquoi la bufferisation et comparaison de performance

On a déjà compris que la bufferisation est une stratégie qui permet de regrouper les données en gros «lots» pour accéder au disque beaucoup moins souvent au lieu d’appeler des opérations des centaines de milliers de fois, en envoyant beaucoup de données d’un coup. Ça donne un gain de performance notable, surtout sur de gros volumes de données.

Quand penser à la performance des E/S

  • Si vous travaillez avec de gros fichiers (gigaoctets, téraoctets — par exemple votre collection de logs accumulée pendant toutes les années de l’entreprise).
  • Si il faut un temps de réponse minimal (par exemple traitement de logs en temps réel).
  • Si le nombre d’accès aux fichiers est énorme (par exemple renommage/copie en masse d’un archive photo).
  • Pour montrer en entretien que vous comprenez les approches modernes d’optimisation (et que vous aimez mesurer la vitesse, pas juste «faire marcher comme ça»).

Dans .NET, la plupart des streams de fichiers ont déjà une bufferisation par défaut, mais parfois il faut un réglage fin ou une stratégie particulière.

Les principaux «acteurs» — quels buffers existent

Classe Buffer par défaut Peut-on changer la taille Applicabilité
FileStream
Oui (4096 octets) Oui (via le constructeur) Stream de fichier de base
BufferedStream
Oui (4096 octets) Oui (via le constructeur) «Wrapper» autour d’un stream
StreamReader/Writer
Oui (1024/1024 octets) Oui (constructeur) Travail avec du texte
  • BufferedStream peut «emballer» un autre stream pour améliorer la performance (par exemple si le stream de base est mal bufferisé ou si on a besoin d’un buffer plus grand).
  • La taille du buffer est un compromis entre vitesse et consommation de mémoire RAM.

2. Exemples :

Comparons trois approches pour copier un gros fichier :

  1. Sans buffer — octet par octet (mauvais exemple, mais parlant !)
  2. Buffer par défautFileStream standard avec CopyTo
  3. Gestion manuelle du buffer — on fournit nous-mêmes le buffer, on optimise la taille

Pour les tests, créons une utilitaire simple de copie de fichiers qu’on ajoutera à notre application. Supposons que le fichier s’appelle BigFile.bin.

class FileCopyBenchmarks
{
    // Copie octet par octet (anti-exemple — ne faites pas comme ça !)
    public static void CopyOneByte(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        int b;
        while ((b = input.ReadByte()) != -1)
        {
            output.WriteByte((byte)b);
        }
    }

    // Copie en utilisant le buffer par défaut de FileStream
    public static void CopyWithDefaultBuffer(string source, string dest)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        input.CopyTo(output); // Utilise un buffer interne (généralement 81920 octets)
    }

    // Copie avec contrôle manuel du buffer
    public static void CopyWithCustomBuffer(string source, string dest, int bufferSize = 1024 * 1024)
    {
        using var input = new FileStream(source, FileMode.Open, FileAccess.Read);
        using var output = new FileStream(dest, FileMode.Create, FileAccess.Write);

        byte[] buffer = new byte[bufferSize];
        int bytesRead;
        while ((bytesRead = input.Read(buffer, 0, buffer.Length)) > 0)
        {
            output.Write(buffer, 0, bytesRead);
        }
    }
}

Laquelle est la plus rapide ? Mesurons le temps d’exécution.

Comment mesurer correctement la performance

Dans .NET, pour mesurer le temps on utilise le plus simplement Stopwatch :

static void Measure(Action action, string description)
{
    var sw = Stopwatch.StartNew();
    action();
    sw.Stop();
    Console.WriteLine($"{description}: {sw.ElapsedMilliseconds} ms");
}

Essayons maintenant de copier le même fichier de différentes façons :

string source = "BigFile.bin";
string dest1 = "copy1.bin";
string dest2 = "copy2.bin";
string dest3 = "copy3.bin";

// Créez à l'avance le fichier BigFile.bin (par exemple 100-500 Mo) ou utilisez n'importe quel fichier volumineux.

Measure(() => FileCopyBenchmarks.CopyOneByte(source, dest1), "CopyOneByte (octet par octet)");
Measure(() => FileCopyBenchmarks.CopyWithDefaultBuffer(source, dest2), "CopyWithDefaultBuffer (standard)");
Measure(() => FileCopyBenchmarks.CopyWithCustomBuffer(source, dest3, 1024 * 1024), "CopyWithCustomBuffer (1 Mo)");

Points dangereux et pièges

  • Si vous lancez les tests plusieurs fois de suite, le cache de l’OS peut «chauffer» le disque et les mesures suivantes seront plus rapides — pour une évaluation réelle il vaut mieux relancer le programme et vider le cache.
  • Si le fichier est petit (10–20 Ko), l’avantage de la bufferisation sera invisible — plus le fichier est grand, plus la différence se voit.
  • Si vous spécifiez un buffer trop grand (par exemple 100 Mo), la consommation de mémoire peut exploser et le reste du système en souffrira.

Visualisation des résultats : tableau

Méthode Temps (ms) pour un fichier de 500 Mo
Octet par octet 100 000+
FileStream standard / CopyTo 1 000 — 5 000
Buffer manuel 1 Mo 700 — 1 200

Chiffres indicatifs, mais la tendance est claire — plus le buffer est grand, moins il y a d’accès disque et meilleure est la vitesse.

3. Anatomie de la gestion manuelle du buffer

Pourquoi vouloir parfois régler la taille du buffer ? Petite analogie simple : vous déménagez dans un nouvel appartement. Vous pouvez transporter les objets cuillère par cuillère, ou prendre une grosse boîte. Mais la boîte a aussi ses limites sinon on ne peut pas la porter !

Comment fonctionne la lecture avec un buffer manuel

// Exemple de gestion manuelle de la taille du buffer
int bufferSize = 1024 * 1024; // 1 Mo
byte[] buffer = new byte[bufferSize];
int read;
while ((read = inputStream.Read(buffer, 0, buffer.Length)) > 0)
{
    outputStream.Write(buffer, 0, read);
}
  • La méthode Read tente de remplir tout le buffer, mais peut retourner moins si le fichier se termine.
  • La taille du buffer est souvent choisie entre 32 Ko et 48 Mo — au-delà, le gain est quasiment nul.
  • N’oubliez pas la mémoire RAM, surtout si il y a beaucoup d’opérations ou de threads qui font pareil.

Expérimenter la taille du buffer

Changez la taille du buffer dans notre exemple (32 Ko, 128 Ko, 1 Mo, 4 Mo) et observez où la performance atteint un pic. En général le «juste milieu» se situe vers 1 Mo.

Scénarios où le buffer manuel est préférable

  • Quand il faut contrôler la quantité de mémoire utilisée (par ex. l’application tourne sur un serveur modeste).
  • Quand le stream n’est pas bufferisé automatiquement (NetworkStream, streams custom).
  • En cas de nombreuses opérations parallèles — on peut donner à chaque thread son buffer de taille optimale.
  • Quand on veut maximiser la vitesse sur un très gros fichier (par ex. transformation d’un gros fichier CSV).

4. Bonnes pratiques et erreurs typiques

Peut-on faire un buffer énorme ? Genre «on a plein de mémoire». Oui, mais pourquoi ? Un buffer trop gros peut au contraire réduire les performances : de la mémoire restera inutilisée et le système peut ralentir à cause du caching.

La bufferisation manuelle n’accélère pas tout. Pour des petits fichiers ou des streams déjà optimisés (par ex. FileStream avec un grand buffer interne) le gain sera minime et le code sera plus complexe.

Piège typique : oublier de fermer un stream ou ne pas gérer une exception — le fichier restera verrouillé. Utilisez using et la gestion d’erreurs (try-catch) quand vous travaillez avec des fichiers.

Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION