40 votes

Pourquoi BufferedReader read() est-il beaucoup plus lent que readLine() ?

Je dois lire un fichier un caractère à la fois et j'utilise la fonction read() méthode de BufferedReader . *

J'ai trouvé que read() est environ 10x plus lent que readLine() . Est-ce normal ? Ou est-ce que je fais quelque chose de mal ?

Voici un benchmark avec Java 7. Le fichier de test d'entrée comporte environ 5 millions de lignes et 254 millions de caractères (~242 Mo) ** :

El read() La méthode prend environ 7000 ms pour lire tous les caractères :

@Test
public void testRead() throws IOException, UnindexableFastaFileException{

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa")));

    long t0= System.currentTimeMillis();
    int c;
    while( (c = fa.read()) != -1 ){
        //
    }
    long t1= System.currentTimeMillis();
    System.err.println(t1-t0); // ~ 7000 ms

}

El readLine() ne prend que ~700 ms :

@Test
public void testReadLine() throws IOException{

    BufferedReader fa= new BufferedReader(new FileReader(new File("chr1.fa")));

    String line;
    long t0= System.currentTimeMillis();
    while( (line = fa.readLine()) != null ){
        //
    }
    long t1= System.currentTimeMillis();
    System.err.println(t1-t0); // ~ 700 ms
}

* Objectif pratique : J'ai besoin de connaître la longueur de chaque ligne, y compris les caractères de nouvelle ligne ( \n o \r\n ) ET la longueur des lignes après les avoir dépouillées. J'ai également besoin de savoir si une ligne commence par l'icône > caractère. Pour un fichier donné, cette opération n'est effectuée qu'une seule fois au début du programme. Puisque les caractères EOL ne sont pas retournés par BufferedReader.readLine() J'ai recours à la read() méthode. S'il existe de meilleures façons de procéder, veuillez nous le signaler.

** Le fichier gzippé est ici http://hgdownload.cse.ucsc.edu/goldenpath/hg19/chromosomes/chr1.fa.gz . Pour ceux qui se posent la question, j'écris une classe pour indexer les fichiers fasta.

36voto

Voo Points 11981

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.

2voto

dariober Points 906

C'est donc le pratique réponse à ma propre question : N'utilisez pas BufferedReader.read() utiliser FileChannel à la place. (Évidemment, je ne réponds pas au POURQUOI que j'ai mis dans le titre). Voici le repère rapide et sale, en espérant que d'autres le trouveront utile :

@Test
public void testFileChannel() throws IOException{

    FileChannel fileChannel = FileChannel.open(Paths.get("chr1.fa"));
    long n= 0;
    int noOfBytesRead = 0;

    long t0= System.nanoTime();

    while(noOfBytesRead != -1){
        ByteBuffer buffer = ByteBuffer.allocate(10000);
        noOfBytesRead = fileChannel.read(buffer);
        buffer.flip();
        while ( buffer.hasRemaining() ) {
            char x= (char)buffer.get();
            n++;
        }
    }
    long t1= System.nanoTime();
    System.err.println((float)(t1-t0) / 1e6); // ~ 250 ms
    System.err.println("nchars: " + n); // 254235640 chars read
}

Avec ~250 ms pour lire le fichier entier caractère par caractère, cette stratégie est considérablement plus rapide que BufferedReader.readLine() (~700 ms), sans parler de read() . Ajout d'instructions if dans la boucle pour vérifier si x == '\n' y x == '>' fait peu de différence. De même, mettre un StringBuilder pour reconstruire les lignes n'affecte pas trop le timing. Cela me convient donc parfaitement (du moins pour l'instant).

Merci à @Marco13 d'avoir mentionné FileChannel.

1voto

hagrawal Points 2143

Merci @Voo pour la correction. Ce que j'ai mentionné ci-dessous est correct à partir de FileReader#read() v/s BufferedReader#readLine() point de vue MAIS non correct de BufferedReader#read() v/s BufferedReader#readLine() point de vue, j'ai donc rayé la réponse.

Utilisation de read() méthode sur BufferedReader n'est pas une bonne idée, cela ne vous causerait aucun tort mais cela gaspille certainement l'objectif de la classe.

~~

Le but ultime de la vie de BufferedReader est de réduire les entrées/sorties en mettant le contenu en mémoire tampon. Vous pouvez lire aquí dans les tutoriels Java. Vous pouvez également remarquer que read() méthode dans BufferedReader est en fait hérité de Reader tandis que readLine() es BufferedReader La méthode propre à l'entreprise.

Si vous voulez utiliser read() alors je dirais que vous devriez utiliser FileReader qui est destiné à cet effet. Vous pouvez lire ici dans les tutoriels Java.

Donc, Je pense que la réponse à votre question est très simple (sans entrer dans le bench-marking et toutes ces explications) -

~~

* Chaque read() est géré par le système d'exploitation sous-jacent et déclenche un accès au disque, une activité réseau ou toute autre opération relativement coûteuse.* Lorsque vous utilisez readLine() alors vous économisez tous ces frais généraux, alors readLine() sera toujours plus rapide que read() Il se peut que le système ne soit pas très efficace pour les petites données, mais il est plus rapide.

0voto

n247s Points 1212

Il n'est pas surprenant de constater cette différence si l'on y réfléchit. Un test consiste à itérer les lignes d'un fichier texte, tandis que l'autre consiste à itérer les caractères.

À moins que chaque ligne ne contienne un caractère, on s'attend à ce que l'option readLine() est bien plus rapide que le read() (bien que, comme le soulignent les commentaires ci-dessus, cela soit discutable puisqu'un BufferedReader met en mémoire tampon l'entrée, alors que la lecture du fichier physique n'est peut-être pas la seule opération nécessitant des performances).

Si vous voulez vraiment tester la différence entre les deux, je vous suggère une configuration où vous itérez sur chaque caractère dans les deux tests. Par exemple, quelque chose comme :

void readTest(BufferedReader r)
{
    int c;
    StringBuilder b = new StringBuilder();
    while((c = r.read()) != -1)
        b.append((char)c);
}

void readLineTest(BufferedReader r)
{
    String line;
    StringBuilder b = new StringBuilder();
    while((line = b.readLine())!= null)
        for(int i = 0; i< line.length; i++)
            b.append(line.charAt(i));
}

Outre ce qui précède, utilisez un "outil de diagnostic des performances de Java" pour évaluer votre code. De plus, lisez ce qui suit comment microbenchmarker un code java .

0voto

simon_ Points 28

Selon la documentation :

Chaque read() fait un appel système coûteux.

Chaque readLine() effectue toujours un appel système coûteux, mais pour un plus grand nombre d'octets à la fois, ce qui réduit le nombre d'appels.

Une situation similaire se produit lorsque nous faisons de la base de données update pour chaque enregistrement à mettre à jour, alors que dans le cas d'une mise à jour par lot, nous n'effectuons qu'un seul appel pour tous les enregistrements.

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