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

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

j'ai trouvé que read() est environ 10x plus lent que readLine() . Est-ce prévu? Ou suis-je en train de faire quelque chose de mal?

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

la méthode read() prend environ 7000 ms à 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
}

* but pratique : je dois connaître la longueur de chaque ligne, y compris les caractères de la nouvelle ligne ( n ou rn ) et la longueur de la ligne après les avoir enlevé. J'ai aussi besoin de savoir si une ligne commence avec le caractère > . Pour un fichier donné, ceci n'est fait qu'une seule fois à la début du programme. Comme les caractères D'EOL ne sont pas retournés par BufferedReader.readLine() , j'utilise la méthode read() . S'il y a de meilleures façons de le faire, veuillez le dire.

* * le fichier gzippé est ici http://hgdownload.cse.ucsc.edu/goldenpath/hg19/chromosomes/chr1.fa.gz . Pour ceux qui pourraient se demander, j'écris un cours pour indexer les fichiers fasta.

38
demandé sur dariober 2016-12-25 23:12:23

6 réponses

l'important pour analyser la performance est d'avoir un benchmark valide avant de commencer. Commençons donc par un benchmark JMH simple qui montre ce que notre performance attendue après warmup serait.

une chose que nous devons considérer est que depuis les systèmes d'exploitation modernes aiment mettre en cache des données de fichier qui est consulté régulièrement nous avons besoin d'un certain moyen pour nettoyer les caches entre les tests. Sur Windows il y a un petit utilitaire qui fait exactement ceci - sur Linux, vous devriez pouvoir le faire en écrivant dans un pseudo-fichier quelque part.

le code apparaît 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écuter 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 effacer les caches pour moi et je suis en cours d'exécution sur un HDD donc c'est pourquoi il est une bonne affaire 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 performances ici après que la JVM se soit installée dans un mode stable. Mais il y a une exception 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 OSR ne fait pas un bon travail ici ou que le JIT est seulement en retard pour faire une différence sur la première itération.

Selon votre cas d'utilisation, cela peut être un gros problème ou négligeable (si vous lisez un millier de fichiers, ça 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'y a pas de solutions générales, bien qu'il y ait des moyens de gérer cela. Un test facile pour voir si nous sommes sur la bonne voie est d'exécuter le code avec l'option -Xcomp qui oblige HotSpot à compiler chaque méthode sur la première invocation. Et en effet, ce faisant, fait disparaître le grand retard 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 de ce qu'est le problème réel (à mon avis est encore tous ces serrures ne sont pas coalescés ou en utilisant l'application de serrures biaisées efficaces), 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 prise sur le problème et il est important de aurait pu être une solution qui n'impliquait pas de changer beaucoup de code).

le code suivant court toujours plus vite que l'un des deux autres - vous pouvez jouer avec la taille du tableau mais c'est étonnamment peu important (probablement parce que contrairement aux autres méthodes read(char[]) n'a pas à acquérir une serrure de sorte que le coût par appel est plus bas 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;
} 

C'est probablement assez bonne performance sage, mais si vous voulais améliorer encore les performances en utilisant un file mapping pourrait (ne compterait 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) aider davantage les performances.

34
répondu Voo 2016-12-26 10:40:53

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, donc, j'ai barré la réponse.

utiliser read() méthode sur BufferedReader n'est pas une bonne idée, il ne serait pas vous causer de tort, mais il est certain que cela gaspille l'objet de la classe.

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

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

, je pense que la réponse à votre question est très simple (sans entrer dans le bench-marking et tous que les explications) -

  • chaque read() est géré par le système d'exploitation sous-jacent et déclenche l'accès disque, l'activité réseau ou toute autre opération relativement coûteuse.
  • quand vous utilisez readLine() alors vous Enregistrez tous ces frais généraux, donc readLine() sera toujours plus rapide que read() , peut ne pas être substantiellement pour les petites données, mais plus rapide.
1
répondu hagrawal 2016-12-27 18:28:18

donc c'est la pratique réponse à ma propre question: N'utilisez pas BufferedReader.read() utilisez FileChannel à la place. (Évidemment, je ne réponds pas à la raison pour laquelle j'ai mis dans le titre). Voici le benchmark rapide et sale, espérons 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 l'ensemble du fichier char par char, cette stratégie est considérablement plus rapide que BufferedReader.readLine() (~700 ms), et encore moins read() . Ajouter des instructions if dans la boucle à vérifier x == '\n' et x == '>' fait peu de différence. Mettre aussi 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.

0
répondu dariober 2016-12-26 19:06:04

il n'est pas surprenant de voir cette différence si on y pense. Un test itère les lignes dans un fichier texte, tandis que l'autre itère les caractères.

sauf si chaque ligne contient un caractère, on s'attend à ce que readLine() soit beaucoup plus rapide que la méthode read() .(bien que comme souligné par les commentaires ci-dessus, il est discutable car un BufferedReader tamponne l'entrée, tandis que la lecture du fichier physique pourrait ne pas être la seule performance prise fonctionnement)

si vous voulez vraiment tester la différence entre les 2 je 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));
}

en plus de ce qui précède, s'il vous plaît utiliser un" outil de diagnostic de performance de Java " pour évaluer votre code. Aussi, readup sur comment microbenchmark code java .

0
répondu n247s 2017-06-07 07:32:35

selon la documentation:

Tous "les 151900920" appel de la méthode fait un coûteux système d'appel.

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

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

0
répondu simon_ 2018-01-22 20:53:08

Java JIT optimise les corps de boucle 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 un (ceci ne s'applique que si les corps des boucles contiennent du code): dans le premier exemple, vous faites une opération par ligne, dans le second, vous en faites une par caractère. Ceci additionne 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.
    }
    
  • deuxième raison: dans la classe BufferedReader , la méthode readLine() n'utilise pas read() dans les coulisses - elle utilise son propre code. La méthode readLine() fait moins d'opérations par caractère pour lire une ligne, qu'il 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 est sur une nouvelle ligne); read() est appelé plus de fois que readLine() .

-1
répondu Luke Melaia 2017-05-23 11:46:16