web-dev-qa-db-fra.com

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

J'ai besoin de lire un fichier un caractère à la fois et j'utilise la méthode read() de BufferedReader. *

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

Voici une référence avec Java 7. Le fichier de test d'entrée a environ 5 millions de lignes et 254 millions de caractères (~ 242 Mo) **:

La méthode read() 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

}

La méthode 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 Ou \r\n ) ET la longueur de la ligne après les avoir retirés. J'ai également besoin de savoir si une ligne commence par le caractère >. Pour un fichier donné, cela n'est effectué qu'une seule fois au démarrage du programme. Puisque les caractères EOL ne sont pas retournés par BufferedReader.readLine() je recourt à la méthode read(). S'il y a de meilleures façons de le faire, veuillez dire.

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

39
dariober

L'important lors de l'analyse des performances est d'avoir un point de référence valide avant de commencer. Commençons donc par un simple benchmark JMH qui montre quelles seraient nos performances attendues après l'échauffement.

Une chose que nous devons considérer est que, puisque les systèmes d'exploitation modernes aiment mettre en cache les données de fichiers auxquelles on accède régulièrement, nous avons besoin d'un moyen de vider les caches entre les tests. Sous Windows, il y a un petit petit utilitaire qui fait exactement cela - sous Linux, vous devriez pouvoir le faire en écrivant quelque part dans un pseudo fichier.

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 je l'exécute 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 aucune différence de performances ici après que la JVM se soit installée en mode stable. Mais il y a une valeur aberrante 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 voyez. La raison la plus probable à laquelle je peux penser est que l'OSR ne fait pas du bon travail ici ou que le JIT ne fonctionne que trop tard pour faire la différence lors de la première itération.

Selon votre cas d'utilisation, cela peut être un gros problème ou négligeable (si vous lisez mille fichiers, cela n'aura 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'y a pas de solutions générales, bien qu'il existe des moyens de gérer cela. Un test facile pour voir si nous sommes sur la bonne voie consiste à exécuter le code avec l'option -Xcomp Qui force HotSpot à compiler chaque méthode lors de la première invocation. Et en effet, cela fait disparaître le retard important lors de la première invocation:

# Warmup Iteration   1: 3.965 s/op
# Warmup Iteration   2: 3.753 s/op

Solution possible

Maintenant que nous avons une bonne idée du problème réel (je suppose que tous ces verrous ne sont pas fusionnés ni n'utilisent l'implémentation efficace de verrous biaisés), la solution est plutôt simple et simple: réduire le nombre d'appels de fonction (donc oui nous aurions pu arriver à cette solution sans tout ce qui précède, mais il est toujours agréable d'avoir une bonne maîtrise du problème et il pourrait y avoir une solution qui n'impliquait pas de changer beaucoup de code).

Le code suivant s'exécute de manière cohérente plus rapidement que l'un des deux autres - vous pouvez jouer avec la taille du tableau mais c'est étonnamment sans importance (probablement parce que contrairement aux autres méthodes, read(char[]) n'a pas besoin d'acquérir un verrou, donc le coût par appel est plus faible pour commencer).

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;
} 

Cela est probablement assez bon en termes de performances, mais si vous vouliez améliorer encore les performances en utilisant un mappage de fichiers pourrait (ne compterait pas sur une amélioration trop importante dans un cas comme celui-ci, mais si vous sachez que votre texte est toujours en ASCII, vous pouvez faire d'autres optimisations) pour améliorer les performances.

35
Voo

Voici donc la réponse pratique à ma propre question: n'utilisez pas BufferedReader.read() utilisez FileChannel à la place. (Évidemment, je ne réponds pas POURQUOI j'ai mis le titre). Voici le point de repère rapide et sale, j'espère 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(). L'ajout d'instructions if dans la boucle pour vérifier x == '\n' Et x == '>' Fait peu de différence. De plus, mettre un StringBuilder pour reconstruire les lignes n'affecte pas trop le timing. C'est donc très bien pour moi (du moins pour l'instant).

Merci à @ Marco13 d'avoir mentionné FileChannel.

2
dariober

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

Utiliser la méthode read() sur BufferedReader n'est pas une bonne idée, cela ne vous causerait aucun mal mais cela gaspille certainement le but de la classe.

Le but de la vie de BufferedReader est de réduire les E/S en tamponnant le contenu. Vous pouvez lire ici dans Java. Vous pouvez également remarquer que la méthode read() dans BufferedReader est en fait héritée de Reader tandis que readLine() est la propre méthode de BufferedReader.

Si vous souhaitez utiliser la méthode read() alors je dirais que vous feriez mieux d'utiliser FileReader, qui est destiné à cet effet. Vous pouvez lire ici dans Java tutoriels.

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

  • Chaque read() est gérée par le système d'exploitation sous-jacent et déclenche l'accès au disque, l'activité réseau ou toute autre opération relativement coûteuse.
  • Lorsque vous utilisez readLine(), vous enregistrez tous ces frais généraux, de sorte que readLine() sera toujours plus rapide que read(), peut ne pas être substantiellement pour les petites données mais plus rapidement.
1
hagrawal

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

À moins que chaque ligne ne contienne un caractère, il est attendu que la readLine() soit bien plus rapide que la méthode read() (bien que comme souligné par les commentaires ci-dessus, elle est discutable car une mémoire tampon BufferedReader l'entrée, alors que la lecture du fichier physique n'est peut-être pas la seule opération de prise de performances)

Si vous voulez vraiment tester la différence entre les 2, je suggérerais une configuration où vous parcourez chaque personnage 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, veuillez utiliser un "outil de diagnostic des performances Java" pour comparer votre code. Aussi, lisez sur comment faire un benchmark Java code .

0
n247s

Java JIT optimise les corps de boucles vides, de sorte que vos boucles ressemblent réellement à ceci:

while((c = fa.read()) != -1);

et

while((line = fa.readLine()) != null);

Je vous suggère de lire sur le benchmarking ici et l'optimisation des boucles ici .


Quant à savoir pourquoi le temps pris diffère:

  • Raison une (cela ne s'applique que si les corps des boucles contiennent du code): Dans le premier exemple, vous effectuez une opération par ligne, dans le deuxièmement, vous en faites un par personnage. Cela ajoute le plus de lignes/caractères que vous avez.

    while((c = fa.read()) != -1){
        //One operation per character.
    }
    
    while((line = fa.readLine()) != null){
        //One operation per line.
    }
    
  • Raison deux: Dans la classe BufferedReader, la méthode readLine() n'utilise pas read() dans les coulisses - il utilise son propre code. La méthode readLine() fait moins d'opérations par caractère pour lire une ligne, qu'il n'en faudrait pour lire une ligne avec la méthode read() - c'est pourquoi readLine() est plus rapide à lire un fichier entier.

  • Troisième raison: Il faut plus d'itérations pour lire chaque caractère que pour lire chaque ligne (sauf si chaque caractère se trouve sur une nouvelle ligne); read() est appelée plus de fois que readLine().

0
Luke Melaia

Selon la documentation:

Chaque appel de méthode read() fait un appel système coûteux.

Cependant, chaque appel de méthode readLine() fait toujours un appel système coûteux, pour plusieurs octets à la fois, donc il y a moins d'appels.

Une situation similaire se produit lorsque nous effectuons la commande de base de données update pour chaque enregistrement que nous voulons mettre à jour, par opposition à une mise à jour par lots, où nous effectuons un appel pour tous les enregistrements.

0
simon_