Parsing efficace de fichiers texte en Java/JEE

Note : vous pouvez joyeusement réutiliser les exemples de code définis ici, je les place sous licence BSD.

Dans votre vie il a pu vous être demandé de devoir parser rapidement de nombreux fichiers texte, et vous vous êtes rendu compte que votre application était lente à le faire. Bref le client n’était pas content, car son appli ne tenait absolument pas la charge. Et pourtant vous aviez utilisé les méthodes standard de la classe String, comme split() et replace().

String.split() et String.replace()

En fait le problème vient du fait que split() et replace() utilisent la plupart du temps des expressions régulières pour fonctionner et ce à chaque fois qu’elles sont invoquées, et celles-ci sont très lentes à s’exécuter. En effet, lors de l’exécution de ces méthodes la première opération invoquée est Pattern.compile() qui est extrêmement coûteuse, puis ce pattern est ensuite exécuté pour retrouver les endroits où découper la chaîne. Pas convaincu ? Je vous invite à tester le programme ci-dessous dans un profiler ou simplement avec les stack traces :



public class Split1 {

        public static void main(String[] args) {
                String toSplit = "String.split()  est  super  lent !!!";

                for (long l = 0; l < Long.MAX_VALUE; l++) {
                        toSplit.split("  ");
                }
        }

}

Alors oui j’ai fait un split sur deux espaces, pour éviter de tomber dans l’optimisation de String.split() qui a été réalisée pour certains cas particuliers, mais celle-ci n’est valable qu’à partir d’OpenJDK 7 dixit GrepCode. Et avec les stacks, vous devriez tomber sur l’une des deux suivantes :


"main" prio=10 tid=0x00007f3e1000a000 nid=0x2617 runnable [0x00007f3e189fa000]
   java.lang.Thread.State: RUNNABLE
        at java.util.regex.Matcher.getTextLength(Matcher.java:1234)
        at java.util.regex.Matcher.reset(Matcher.java:308)
        at java.util.regex.Matcher.(Matcher.java:228)
        at java.util.regex.Pattern.matcher(Pattern.java:1088)
        at java.util.regex.Pattern.split(Pattern.java:1197)
        at java.lang.String.split(String.java:2313)
        at java.lang.String.split(String.java:2355)
        at Split1.main(Split1.java:9)

Ou encore ceci :


"main" prio=10 tid=0x00007f3e1000a000 nid=0x2617 runnable [0x00007f3e189fa000]
   java.lang.Thread.State: RUNNABLE
        at java.util.regex.Matcher.find(Matcher.java:592)
        at java.util.regex.Pattern.split(Pattern.java:1200)
        at java.lang.String.split(String.java:2313)
        at java.lang.String.split(String.java:2355)
        at Split1.main(Split1.java:9)

Maintenant, je vais faire le même programme mais en utilisant un split custom :


import java.util.*;

public class Split2 {

        private final static String[] mySplit(final String str, final String splitChunk) {
                // A faire pour être propre : ajouter un contrôle qu'aucune des deux chaînes n'est null
                List res = new ArrayList();

                String chunk = str;
                int index = chunk.indexOf(splitChunk);
                int len = splitChunk.length();

                while (index != -1) {
                        res.add(chunk.substring(0, index));
                        chunk = chunk.substring(index + len);
                        index = chunk.indexOf(splitChunk);
                }

                res.add(chunk);

                return res.toArray(new String[res.size()]);
        }

        public static void main(String[] args) {
                String toSplit = "String.split()  est  super  lent !!!";

                for (long l = 0; l < Long.MAX_VALUE; l++) {
                        mySplit(toSplit, "  ");
                }
        }

}

Cette fois, vous ne verrez que les stacks ci-dessous :


"main" prio=10 tid=0x00007f04dc00a000 nid=0x2712 runnable [0x00007f04e3d3b000]
   java.lang.Thread.State: RUNNABLE
        at java.lang.String.indexOf(String.java:1698)
        at java.lang.String.indexOf(String.java:1678)
        at Split2.mySplit(Split2.java:15)
        at Split2.main(Split2.java:28)

Et si vous avez du matériel pour faire du micro-benchmark, ce que je n’ai pas à disposition à l’heure où j’écris cet article, vous observerez que le programme numéro deux est bien plus rapide que le premier. Pour ce faire sous Unix vous pouvez utiliser la commande time java Split1 et comparer avec time java Split2.

Pour ceux qui ne veulent pas implémenter une méthode et devoir la maintenir, vous pouvez utiliser la classe StringUtils de la librairie Apache Commons Util, qui contient notamment la méthode splitByWholeSeparator pour le split, et replace pour les remplacements.

Usez et abusez du indexOf()

Dans certains cas vous n’aurez pas besoin d’utiliser de split() ou de replace() mais vous devrez rechercher des morceaux de texte comme délimiteur. Dans ce cas n’hésitez pas à utiliser le indexOf() qui est particulièrement rapide, étant donné qu’il se contente de parcourir un tableau de caractères et de faire des comparaisons.

N’hésitez pas à utiliser des caches au sein de votre parseur

Il m’est arrivé également de devoir parser des dates au sein d’un fichier, avec la même date apparaissant de nombreuses fois dans celui-ci. Là encore la lecture d’une date est très coûteuse, aussi en stockant au sein d’une Map une correspondance entre la date lue et la date véritable, on gagnait là encore beaucoup au niveau de la performance. N’hésitez pas à faire de même pour tout élément coûteux à parser et que vous retrouvez de manière répétée au sein de votre fichier. Je ne peux pas donner de chiffre mais il y a souvent beaucoup à gagner !

Cet article vous a plu ? Vous aimerez sûrement aussi :

Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.

Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.

Son Twitter Son LinkedIn

gojul

Recent Posts

MICI au travail : Le handicap invisible qui révèle des forces insoupçonnées

Les maladies inflammatoires chroniques de l’intestin ou "MICI" sont invisibles, mais leurs impacts sur la…

2 jours ago

Exploiter les NPUs pour de l’IA embarquée dans les applis webs

Depuis l'été, j'ai un Pixel qui intègre à la fois un TPU (Tensor Processing Unit)…

6 jours ago

Qcm saison hiver 2024 : toutes les infos.

On se retrouve dans un nouvel article avec toutes les infos sur cette nouvelle saison…

3 semaines ago

L’inclusion numérique est essentielle.

Pourquoi l’inclusion numérique est essentielle : le point avec Mathieu Froidure. Dans un monde de…

4 semaines ago

Communauté Tech et féminine : Interview avec Helvira de Motiv’her

Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…

1 mois ago