L’article a été mis à jour suite à un complément d’information fourni par le développeur principal de TinyCsvParser, cf conclusion
Le traitement d’un fichier csv de plusieurs Go peut vite être coûteux en terme de performance.
Pour rappel un fichier csv n’est pas seulement un format qui sépare ses colonnes par un caractère, mais c’est aussi:
des entêtes présentes ou non,
des colonnes parfois inexistantes,
des lignes vides,
des guillemets pour représenter une colonne,
des guillemets dans des guillemets pour représenter des guillemets dans une colonne…
…
Bref, la liste est encore longue et ça peut vite devenir un vrai casse tête de gérer tous les cas.
A cela s’ajoute qu’il faut la plupart du temps lier les colonnes à des objets de notre code, que dans le cas d’un très gros fichier il n’est pas envisageable de charger le fichier en mémoire et qu’il faudra lire à même le stream ce qui peut apporter d’autres problématiques (heureusement en .NET cette dernière problématique reste extrêmement simple à solutionner) cela peut devenir relativement compliqué.
Bien entendu d’autres se sont pris la tête là-dessus, c’est pour cette raison qu’il existe un grand nombre de framework de lecture/écriture de fichier csv qui gère la totalité des problématiques liées à ce format.
TextFieldParser
Le framework .NET propose une solution de base avec TextFieldParser.
Doit-on d’office exclure CsvHelper?
Non, comme toujours en développement rien n’est blanc ou noir.
Dans ce cas spécifique (un csv propre, une configuration de base et une lecture d’un gros fichier)TinyCsvParser est préférable en revanche dans d’autres scénarii CsvHelper apporte des fonctionnalités très intéressantes (comme l’auto-mapping) qui peuvent faire gagner un précieux temps de développement et de maintenance.
Bref cela dépend de la situation comme toujours.
Mais comment expliquer de tels décalages ?
Principalement cela est dû au traitement des lignes du CSV pour gérer tous les cas et la création des entités.
Nous avons tendance à se ruer sur des framework pour des choses simples, ces framework sont là pour gérer tous les cas ce qui apporte un confort de développement, de la fiabilité mais malheureusement aussi une lourdeur dans les traitements.
J’ai récemment dû parser un fichier de 30Go “propre” (virgule + parfois des guillemets) dont je connais parfaitement le format, et j’ai voulu voir combien coûte “la gestion de tous les cas”.
L’algorithme d’analyse des lignes csv est simple et limitée mais correspond à mon besoin.
Et voici le résultat :
Method
Mean
%
TextFieldParser
6,617.43 ms
0%
CsvHelper
4,018.37 ms
-39 %
TinyCsvParser
1,062.29 ms
-84 %
Custom
1,083.97 ms
-83 %
Comment TinyCsvParser peut-il encore être plus performant ? Simplement car ce framework utilise de base le parallélisme. Nous allons en faire de même pour la science.
Solution personnalisée avec parallélisme
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[Benchmark]publicvoidCustomParallel(){usingvarstreamReader=newStreamReader(_csvFilePath);varenumerator=newStreamReaderEnumerable(streamReader);varpo=newParallelOptions{// just for demo
MaxDegreeOfParallelism=Environment.ProcessorCount};varaction=newAction<string>(line=>{Foo.CreateFromCsvLine(line);// ...
});Parallel.ForEach(enumerator,po,action);}
/// <summary>
/// This method comes from <see href="!:https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1.etenumerator">msdn</see>
/// </summary>
publicclassStreamReaderEnumerable:IEnumerable<string>{privatereadonlyStreamReader_sr;publicStreamReaderEnumerable(StreamReaderstreamReader){_sr=streamReader;}// Must implement GetEnumerator, which returns a new StreamReaderEnumerator.
publicIEnumerator<string>GetEnumerator(){returnnewStreamReaderEnumerator(_sr);}// Must also implement IEnumerable.GetEnumerator, but implement as a private method.
privateIEnumeratorGetEnumerator1(){returnGetEnumerator();}IEnumeratorIEnumerable.GetEnumerator(){returnGetEnumerator1();}}/// <summary>
/// This method comes from <see href="!:https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.ienumerable-1.etenumerator">msdn</see>
/// </summary>
publicclassStreamReaderEnumerator:IEnumerator<string>{privatereadonlyStreamReader_sr;publicStreamReaderEnumerator(StreamReaderstreamReader){_sr=streamReader;}privatestring_current;// Implement the IEnumerator(T).Current publicly, but implement
// IEnumerator.Current, which is also required, privately.
publicstringCurrent{get{if(_sr==null||_current==null){thrownewInvalidOperationException();}return_current;}}privateobjectCurrent1=>this.Current;objectIEnumerator.Current=>Current1;// Implement MoveNext and Reset, which are required by IEnumerator.
publicboolMoveNext(){_current=_sr.ReadLine();return_current!=null;}publicvoidReset(){_sr.DiscardBufferedData();_sr.BaseStream.Seek(0,SeekOrigin.Begin);_current=null;}// Implement IDisposable, which is also implemented by IEnumerator(T).
privatebool_disposedValue;publicvoidDispose(){Dispose(true);GC.SuppressFinalize(this);}protectedvirtualvoidDispose(booldisposing){if(!_disposedValue){if(disposing){// Dispose of managed resources.
}_current=null;if(_sr!=null){_sr.Close();_sr.Dispose();}}_disposedValue=true;}~StreamReaderEnumerator(){Dispose(false);}}
Method
Mean
%
TextFieldParser
6,617.43 ms
0%
CsvHelper
4,018.37 ms
-39 %
TinyCsvParser
1,062.29 ms
-84 %
Custom
1,083.97 ms
-83 %
CustomParallel
632.97 ms
-90 %
Le temps est divisé par 2 par rapport à TinyCsvParser. Au final, sur mon fichier de 30Go je suis passé de 7min à 3min20 de traitement de manière simple.
Conclusion
Les performances d’une solution personnalisée laissent rêveur, néanmoins ce code ne répond qu’à un seul et unique cas: les très gros fichiers csv propre et simple.
Mais dans le cas où vous ayez un grand nombre de fichier csv avec des “qualités” variables, “il est fortement recommandé” de passer par un framework tel que CsvHelper ou TinyCsvParser où un grand nombre de bons développeurs ont pu analyser les performances de chaque ligne de code permettant de gérer tout les cas.
NB: Il est intéressant de se rendre compte que le classement est totalement chamboulé sur des petits fichiers csv (exemple avec un csv de 10 lignes):
Method
Mean
%
TextFieldParser
253.18 us
0%
CsvHelper
991.40 us
+291 %
TinyCsvParser
653.78 us
+158 %
Custom
50.99 us
-79 %
CustomParallel
113.25 us
-55 %
Ce qui prouve encore qu’il n’y a pas de magie en développement, que tout dépend du contexte et annexement que le parallélisme est très souvent profitable et recommandé mais peut aussi être un piège.
Au delà de ça, son retour (présent ici) me fait revoir/nuancer le message de ce post.
En effet la réponse pour savoir si oui ou non un framework est à utiliser pour ce besoin spécifique est plus compliqué que de premier abord (plus compliqué que beaucoup = framework / un cas = personnalisé ou framework).
J’ai voulu dans un premier temps traiter le sujet ici, mais après réflexion beaucoup de paramètres rentrent en jeu (le contexte, le budget, les compétences, le risque, les resources, le besoin…), cela fera l’objet d’un post annexe plus globale qui apportera non pas une réponse (je pense que cela est impossible) mais un point de vue sur la question.