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().
Débloque les + belles offres tech en 10 mins
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.
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.
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 !
Débloque les + belles offres tech en 10 mins
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.
Les maladies inflammatoires chroniques de l’intestin ou "MICI" sont invisibles, mais leurs impacts sur la…
Depuis l'été, j'ai un Pixel qui intègre à la fois un TPU (Tensor Processing Unit)…
On se retrouve dans un nouvel article avec toutes les infos sur cette nouvelle saison…
Pourquoi l’inclusion numérique est essentielle : le point avec Mathieu Froidure. Dans un monde de…
Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…