Why is BufferedReader read() much slower than readLine()?

后端 未结 6 765
不思量自难忘°
不思量自难忘° 2020-12-29 18:45

I need to read a file one character at a time and I\'m using the read() method from BufferedReader. *

I found that read() is a

6条回答
  •  隐瞒了意图╮
    2020-12-29 19:03

    The important thing when analyzing performance is to have a valid benchmark before you start. So let's start with a simple JMH benchmark that shows what our expected performance after warmup would be.

    One thing we have to consider is that since modern operating systems like to cache file data that is accessed regularly we need some way to clear the caches between tests. On Windows there's a small little utility that does just this - on Linux you should be able to do it by writing to some pseudo file somewhere.

    The code then looks as follows:

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

    and if we run it with

    chcp 65001 # set codepage to utf-8
    mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar
    

    we get the following results (about 2 seconds are needed to clear the caches for me and I'm running this on a HDD so that's why it's a good deal slower than for you):

    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! As expected there's no performance difference here at all after the JVM has settled into a stable mode. But there is one outlier in the readCharTest method:

    # Warmup Iteration   1: 6.186 s/op
    # Warmup Iteration   2: 3.744 s/op
    

    which is exaclty the problem you're seeing. The most likely reason I can think of is that OSR isn't doing a good job here or that the JIT is only running too late to make a difference on the first iteration.

    Depending on your use case this might be a big problem or negligible (if you're reading a thousand files it won't matter, if you're only reading one this is a problem).

    Solving such a problem is not easy and there are no general solutions, although there are ways to handle this. One easy test to see if we're on the right track is to run the code with the -Xcomp option which forces HotSpot to compile every method on the first invocation. And indeed doing so, causes the large delay at the first invocation to disappear:

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

    Possible solution

    Now that we have a good idea what the actual problem is (my guess is still all those locks neither being coalesced nor using the efficient biased locks implementation), the solution is rather straight forward and simple: Reduce the number of function calls (so yes we could've arrived at this solution without everything above, but it's always nice to have a good grip on the problem and there might have been a solution that didn't involve changing much code).

    The following code runs consistently faster than either of the other two - you can play with the array size but it's surprisingly unimportant (presumably because contrary to the other methods read(char[]) does not have to acquire a lock so the cost per call is lower to begin with).

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

    This is most likely good enough performance wise, but if you wanted to improve performance even further using a file mapping might (wouldn't count on too large an improvement in a case such as this, but if you know that your text is always ASCII, you could make some further optimizations) further help performance.

提交回复
热议问题