114 votes

Lecture de fichiers texte volumineux avec des flux en C#

J'ai l'agréable tâche de trouver comment gérer les gros fichiers qui sont chargés dans l'éditeur script de notre application (c'est comme VBA pour notre produit interne pour les macros rapides). La plupart des fichiers font environ 300-400 Ko, ce qui est un bon chargement. Mais lorsqu'ils dépassent 100 Mo, le processus a du mal à fonctionner (comme on peut s'y attendre).

Le fichier est lu et placé dans une RichTextBox qui est ensuite parcourue - ne vous inquiétez pas trop de cette partie.

Le développeur qui a écrit le code initial utilise simplement un StreamReader et fait

[Reader].ReadToEnd()

ce qui peut prendre un certain temps.

Ma tâche consiste à décomposer ce bout de code, à le lire par morceaux dans un tampon et à afficher une barre de progression avec une option d'annulation.

Quelques hypothèses :

  • La plupart des fichiers seront de 30 à 40 Mo
  • Le contenu du fichier est du texte (pas binaire), certains sont au format Unix, d'autres au format DOS.
  • Une fois le contenu récupéré, nous déterminons quel est le terminateur utilisé.
  • Personne ne se préoccupe, une fois qu'il est chargé, du temps que prend le rendu dans la richtextbox. C'est juste le chargement initial du texte.

Maintenant, les questions :

  • Puis-je simplement utiliser StreamReader, puis vérifier la propriété Length (donc ProgressMax) et lancer une lecture pour une taille de tampon définie et itérer dans une boucle while ? WHILST dans un travailleur d'arrière-plan, pour ne pas bloquer le fil d'exécution principal de l'interface utilisateur ? Puis renvoyer le stringbuilder au thread principal une fois qu'il est terminé.
  • Le contenu sera envoyé dans un StringBuilder. Puis-je initialiser le StringBuilder avec la taille du flux si la longueur est disponible ?

S'agit-il (selon votre opinion professionnelle) de bonnes idées ? J'ai eu quelques problèmes dans le passé avec la lecture du contenu de Streams, parce qu'il manque toujours les derniers octets ou quelque chose comme ça, mais je poserai une autre question si c'est le cas.

33 votes

Des fichiers de 30-40MB script ? Nom d'un maquereau ! Je n'aimerais pas avoir à réviser ce code...

0 votes

Je sais que cette question est assez ancienne mais je l'ai trouvée l'autre jour et j'ai testé la recommandation pour MemoryMappedFile et c'est de loin la méthode la plus rapide. A titre de comparaison, la lecture d'un fichier de 7 616 939 lignes et 345 Mo via une méthode de lecture en ligne prend plus de 12 heures sur ma machine, alors que le même chargement et la lecture via MemoryMappedFile prennent 3 secondes.

0 votes

C'est juste quelques lignes de code. Voyez cette bibliothèque que j'utilise pour lire des fichiers de 25 Go et plus. github.com/Agenty/FileReader

206voto

Eric J. Points 73338

Vous pouvez améliorer la vitesse de lecture en utilisant un BufferedStream, comme ceci :

using (FileStream fs = File.Open(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (BufferedStream bs = new BufferedStream(fs))
using (StreamReader sr = new StreamReader(bs))
{
    string line;
    while ((line = sr.ReadLine()) != null)
    {

    }
}

Mise à jour de mars 2013

J'ai récemment écrit du code pour lire et traiter (rechercher du texte dans) des fichiers texte d'environ 1 Go (beaucoup plus gros que les fichiers concernés ici) et j'ai obtenu un gain de performance significatif en utilisant un modèle producteur/consommateur. La tâche du producteur lit les lignes de texte en utilisant la fonction BufferedStream et les a transmis à une tâche de consommation distincte qui a effectué la recherche.

J'en ai profité pour apprendre le TPL Dataflow, qui est très bien adapté pour coder rapidement ce modèle.

Pourquoi BufferedStream est plus rapide

Un tampon est un bloc d'octets en mémoire utilisé pour mettre en cache des données, réduisant ainsi le nombre d'appels au système d'exploitation. Les tampons améliorent les performances de lecture et d'écriture. Une mémoire tampon peut être utilisée pour la lecture ou l'écriture, mais jamais pour les deux simultanément. Les méthodes Read et Write de BufferedStream maintiennent automatiquement la mémoire tampon.

MISE À JOUR de décembre 2014 : votre kilométrage peut varier

D'après les commentaires, FileStream devrait utiliser un fichier BufferedStream en interne. Au moment où cette réponse a été fournie pour la première fois, j'ai mesuré une augmentation significative des performances en ajoutant un BufferedStream. À l'époque, je visais .NET 3.x sur une plate-forme 32 bits. Aujourd'hui, en ciblant .NET 4.5 sur une plate-forme 64 bits, je ne vois aucune amélioration.

Related

J'ai rencontré un cas où la diffusion d'un gros fichier CSV généré vers le flux de réponse d'une action ASP.Net MVC était très lente. L'ajout d'un BufferedStream a permis de multiplier les performances par 100 dans ce cas. Pour en savoir plus, voir Sortie sans tampon très lente

13 votes

Mec, BufferedStream fait toute la différence. +1 :)

0 votes

Beaucoup plus rapide que streamReader.ReadLine seulement... Merci beaucoup Eric. Pouvez-vous également expliquer pourquoi c'est tellement plus rapide ou m'indiquer une ressource où je peux lire à ce sujet. Merci d'avance.

2 votes

La demande de données auprès d'un sous-système d'E/S a un coût. Dans le cas des disques rotatifs, il faut parfois attendre que le plateau tourne pour se mettre en position de lire le prochain morceau de données, ou pire, attendre que la tête du disque bouge. Bien que les disques SSD n'aient pas de pièces mécaniques pour ralentir les choses, il y a toujours un coût par opération d'entrée pour y accéder. Les flux tamponnés lisent plus que ce que le StreamReader demande, ce qui réduit le nombre d'appels au système d'exploitation et, en fin de compte, le nombre de demandes d'entrée/sortie distinctes.

33voto

user4023224 Points 11

Si vous lisez le performances et statistiques de référence sur ce site web vous verrez que le moyen le plus rapide de lire (parce que la lecture, l'écriture et le traitement sont tous différents) un fichier texte est le bout de code suivant :

using (StreamReader sr = File.OpenText(fileName))
{
    string s = String.Empty;
    while ((s = sr.ReadLine()) != null)
    {
        //do your stuff here
    }
}

Au total, environ 9 méthodes différentes ont été évaluées, mais celle-là semble avoir été la meilleure la plupart du temps, même sur le lecteur en mémoire tampon comme l'ont mentionné d'autres lecteurs.

2 votes

Cela a bien fonctionné pour dépouiller un fichier postgres de 19 Go et le traduire en syntaxe sql dans plusieurs fichiers. Merci au gars de postgres qui n'a jamais exécuté mes paramètres correctement. /soupir

0 votes

La différence de performance semble se révéler payante pour les fichiers très volumineux, comme ceux de plus de 150 Mo (vous devriez également utiliser un fichier de type StringBuilder pour les charger en mémoire, se charge plus rapidement car il ne crée pas une nouvelle chaîne à chaque fois que vous ajoutez des caractères)

16voto

Christian Hayter Points 17999

Vous dites qu'on vous a demandé d'afficher une barre de progression pendant le chargement d'un gros fichier. Est-ce parce que les utilisateurs veulent vraiment voir le pourcentage exact de chargement du fichier, ou simplement parce qu'ils veulent un retour visuel sur ce qui se passe ?

Si cette dernière affirmation est vraie, la solution devient alors beaucoup plus simple. Il suffit de faire reader.ReadToEnd() sur un fil d'arrière-plan, et affiche une barre de progression en forme de marque au lieu d'une barre normale.

Je soulève ce point car, d'après mon expérience, c'est souvent le cas. Si vous écrivez un programme de traitement des données, les utilisateurs seront certainement intéressés par un pourcentage de réussite, mais pour les mises à jour simples mais lentes de l'interface utilisateur, il est plus probable qu'ils veuillent simplement savoir que l'ordinateur n'a pas planté :-).

3 votes

Mais l'utilisateur peut-il annuler l'appel ReadToEnd ?

1 votes

@Tim, bien vu. Dans ce cas, nous sommes de retour à la StreamReader boucle. Cependant, elle sera toujours plus simple car il n'y a pas besoin de lire en avant pour calculer l'indicateur de progression.

9voto

Tufo Points 258

Utilisez un travailleur en arrière-plan et ne lisez qu'un nombre limité de lignes. Ne lisez la suite que lorsque l'utilisateur fait défiler la page.

Et essayez de ne jamais utiliser ReadToEnd(). C'est l'une des fonctions dont on se dit "pourquoi l'ont-ils faite ?" ; c'est une scriptdes enfants'. qui fonctionne bien avec les petites choses, mais comme vous le voyez, il est nul pour les gros fichiers...

Les personnes qui vous disent d'utiliser StringBuilder doivent lire le MSDN plus souvent :

Considérations sur les performances
Les méthodes Concat et AppendFormat permettent toutes deux de concaténer de nouvelles données à un objet String ou StringBuilder existant. Une opération de concaténation d'un objet String crée toujours un nouvel objet à partir de la chaîne existante et des nouvelles données. Un objet StringBuilder maintient un tampon pour accueillir la concaténation de nouvelles données. Les nouvelles données sont ajoutées à la fin du tampon s'il reste de la place ; sinon, un nouveau tampon plus grand est alloué, les données du tampon d'origine sont copiées dans le nouveau tampon, puis les nouvelles données sont ajoutées au nouveau tampon. Les performances d'une opération de concaténation pour un objet String ou StringBuilder dépendent de la fréquence à laquelle une allocation de mémoire se produit.
Une opération de concaténation String alloue toujours de la mémoire, alors qu'une opération de concaténation StringBuilder n'alloue de la mémoire que si le tampon de l'objet StringBuilder est trop petit pour accueillir les nouvelles données. Par conséquent, la classe String est préférable pour une opération de concaténation si un nombre fixe d'objets String sont concaténés. Dans ce cas, les opérations de concaténation individuelles peuvent même être combinées en une seule opération par le compilateur. Un objet StringBuilder est préférable pour une opération de concaténation si un nombre arbitraire de chaînes sont concaténées ; par exemple, si une boucle concatène un nombre aléatoire de chaînes d'entrée utilisateur.

Cela signifie que énorme allocation de la mémoire, ce qui devient une grande utilisation du système de fichiers d'échange, qui simule des sections de votre disque dur pour agir comme la mémoire RAM, mais un disque dur est très lent.

L'option StringBuilder semble parfaite pour ceux qui utilisent le système en tant que mono-utilisateur, mais lorsque vous avez deux utilisateurs ou plus qui lisent de gros fichiers en même temps, vous avez un problème.

0 votes

Malheureusement, en raison de la façon dont les macros fonctionnent, le flux entier doit être chargé. Comme je l'ai dit, ne vous inquiétez pas pour la partie texte riche. C'est le chargement initial que nous voulons améliorer.

0 votes

Vous pouvez donc travailler par parties, lire les X premières lignes, appliquer la macro, lire les X secondes lignes, appliquer la macro, et ainsi de suite... si vous expliquez ce que fait cette macro, nous pourrons vous aider avec plus de précision.

6voto

ChaosPandion Points 37025

Cela devrait être suffisant pour vous permettre de commencer.

class Program
{        
    static void Main(String[] args)
    {
        const int bufferSize = 1024;

        var sb = new StringBuilder();
        var buffer = new Char[bufferSize];
        var length = 0L;
        var totalRead = 0L;
        var count = bufferSize; 

        using (var sr = new StreamReader(@"C:\Temp\file.txt"))
        {
            length = sr.BaseStream.Length;               
            while (count > 0)
            {                    
                count = sr.Read(buffer, 0, bufferSize);
                sb.Append(buffer, 0, count);
                totalRead += count;
            }                
        }

        Console.ReadKey();
    }
}

7 votes

Je déplacerais le "var buffer = new char[1024]" hors de la boucle : il n'est pas nécessaire de créer un nouveau tampon à chaque fois. Mettez-le juste avant "while (count > 0)".

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X