L'important, lorsqu'on analyse les performances, est de disposer d'un repère valable avant de commencer. Commençons donc par un simple benchmark JMH qui montre les performances attendues après le warm-up.
Une chose à prendre en compte est que, puisque les systèmes d'exploitation modernes aiment mettre en cache les données des fichiers auxquels on accède régulièrement, nous devons trouver un moyen de vider les caches entre les tests. Sous Windows, il existe un petit utilitaire qui fait exactement cela - sous Linux, vous devriez pouvoir le faire en écrivant dans un pseudo-fichier quelque part.
Le code se présente alors comme suit :
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Mode;
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
@BenchmarkMode(Mode.AverageTime)
@Fork(1)
public class IoPerformanceBenchmark {
private static final String FILE_PATH = "test.fa";
@Benchmark
public int readTest() throws IOException, InterruptedException {
clearFileCaches();
int result = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
int value;
while ((value = reader.read()) != -1) {
result += value;
}
}
return result;
}
@Benchmark
public int readLineTest() throws IOException, InterruptedException {
clearFileCaches();
int result = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
String line;
while ((line = reader.readLine()) != null) {
result += line.chars().sum();
}
}
return result;
}
private void clearFileCaches() throws IOException, InterruptedException {
ProcessBuilder pb = new ProcessBuilder("EmptyStandbyList.exe", "standbylist");
pb.inheritIO();
pb.start().waitFor();
}
}
et si nous l'exécutons avec
chcp 65001 # set codepage to utf-8
mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar
nous obtenons les résultats suivants (environ 2 secondes sont nécessaires pour vider les caches pour moi et j'exécute ceci sur un disque dur, c'est pourquoi c'est beaucoup plus lent que pour vous) :
Benchmark Mode Cnt Score Error Units
IoPerformanceBenchmark.readLineTest avgt 20 3.749 ± 0.039 s/op
IoPerformanceBenchmark.readTest avgt 20 3.745 ± 0.023 s/op
Surprise ! Comme prévu, il n'y a pas de différence de performance après que la JVM se soit installée dans un mode stable. Mais il y a une anomalie dans la méthode readCharTest :
# Warmup Iteration 1: 6.186 s/op
# Warmup Iteration 2: 3.744 s/op
ce qui est exactement le problème que vous rencontrez. La raison la plus probable à laquelle je peux penser est que OSR ne fait pas un bon travail ici ou que le JIT s'exécute trop tard pour faire une différence à la première itération.
En fonction de votre cas d'utilisation, cela peut être un gros problème ou négligeable (si vous lisez un millier de fichiers, cela n'a pas d'importance, si vous n'en lisez qu'un, c'est un problème).
Résoudre un tel problème n'est pas facile et il n'existe pas de solution générale, bien qu'il y ait des moyens de s'en sortir. Un test facile pour voir si nous sommes sur la bonne voie est d'exécuter le code avec la commande -Xcomp
qui force HotSpot à compiler chaque méthode lors de la première invocation. Et en faisant cela, le grand retard à la première invocation disparaît :
# Warmup Iteration 1: 3.965 s/op
# Warmup Iteration 2: 3.753 s/op
Solution possible
Maintenant que nous avons une bonne idée de ce qu'est le problème réel (je pense que tous ces verrous ne sont pas coalisés et n'utilisent pas l'implémentation efficace des verrous biaisés), la solution est plutôt directe et simple : Réduire le nombre d'appels de fonction (oui, nous aurions pu arriver à cette solution sans tout ce qui précède, mais c'est toujours bien d'avoir une bonne prise sur le problème et il aurait pu y avoir une solution qui n'implique pas de changer beaucoup de code).
Le code suivant s'exécute systématiquement plus rapidement que les deux autres - vous pouvez jouer avec la taille du tableau mais cela n'a étonnamment aucune importance (probablement parce que contrairement aux autres méthodes read(char[])
n'a pas besoin d'acquérir un verrou, le coût par appel est donc plus faible au départ).
private static final int BUFFER_SIZE = 256;
private char[] arr = new char[BUFFER_SIZE];
@Benchmark
public int readArrayTest() throws IOException, InterruptedException {
clearFileCaches();
int result = 0;
try (BufferedReader reader = new BufferedReader(new FileReader(FILE_PATH))) {
int charsRead;
while ((charsRead = reader.read(arr)) != -1) {
for (int i = 0; i < charsRead; i++) {
result += arr[i];
}
}
}
return result;
}
Les performances sont sans doute suffisantes, mais si vous souhaitez améliorer encore les performances en utilisant un fichier correspondance des fichiers pourrait (je ne compterais pas sur une amélioration trop importante dans un cas comme celui-ci, mais si vous savez que votre texte est toujours ASCII, vous pourriez faire quelques optimisations supplémentaires) améliorer encore les performances.