为什么BufferedReader read()比readLine()慢得多?

dar*_*ber 40 java benchmarking bufferedreader

我需要一次读取一个文件,我正在使用该read()方法BufferedReader.*

我发现这read()比约慢10倍readLine().这是预期的吗?或者我做错了什么?

这是Java 7的基准测试.输入测试文件大约有500万行和2.54亿个字符(~242 MB)**:

read()方法需要大约7000毫秒来读取所有字符:

@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

}
Run Code Online (Sandbox Code Playgroud)

readLine()方法只需约700毫秒:

@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
}
Run Code Online (Sandbox Code Playgroud)

*实用目的:我需要知道每一行的长度,包括换行符(\n\r\n)和剥离后的行长度.我还需要知道一行是否以>字符开头.对于给定的文件,这只在程序开始时完成一次.因为BufferedReader.readLine()我正在使用该read()方法返回EOL字符.如果有更好的方法,请说.

**gzip压缩文件位于http://hgdownload.cse.ucsc.edu/goldenpath/hg19/chromosomes/chr1.fa.gz.对于那些可能想知道的人,我正在写一个类来索引fasta文件.

Voo*_*Voo 36

分析性能时,重要的是在开始之前获得有效的基准.因此,让我们从一个简单的JMH基准测试开始,该基准测试显示我们在预热后的预期性能.

我们必须考虑的一件事是,由于现代操作系统喜欢缓存定期访问的文件数据,我们需要一些方法来清除测试之间的缓存.在Windows上有一个小实用程序可以做到这一点 - 在Linux上,您应该能够通过在某处写入某些伪文件来实现.

然后代码如下所示:

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

如果我们运行它

chcp 65001 # set codepage to utf-8
mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar
Run Code Online (Sandbox Code Playgroud)

我们得到以下结果(需要大约2秒才能清除缓存,我在硬盘上运行它,这就是为什么它比你慢得多):

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
Run Code Online (Sandbox Code Playgroud)

惊喜!正如预期的那样,在JVM进入稳定模式后,这里完全没有性能差异.但是readCharTest方法中有一个异常值:

# Warmup Iteration   1: 6.186 s/op
# Warmup Iteration   2: 3.744 s/op
Run Code Online (Sandbox Code Playgroud)

这是你所看到的问题.我能想到的最可能的原因是OSR在这里做得不好或者JIT只是运行得太晚而无法在第一次迭代中产生影响.

根据您的使用情况,这可能是一个大问题或可忽略不计(如果您正在阅读一千个文件,那么无关紧要,如果您只是阅读一个这是一个问题).

解决这样的问题并不容易,并且没有通用的解决方案,尽管有办法解决这个问题.一个简单的测试,看看我们是否在正确的轨道上运行代码与-Xcomp强制HotSpot在第一次调用时编译每个方法的选项.确实这样做会导致第一次调用的大延迟消失:

# Warmup Iteration   1: 3.965 s/op
# Warmup Iteration   2: 3.753 s/op
Run Code Online (Sandbox Code Playgroud)

可能解决方案

现在我们已经知道实际问题是什么了(我的猜测仍然是所有这些锁都没有被合并,也没有使用有效的偏置锁实现),解决方案相当简单明了:减少函数调用次数(所以是的我们可以在没有上述所有内容的情况下找到这个解决方案,但是对问题有一个很好的把握并且可能有一个不涉及更改代码的解决方案总是很好.

以下代码的运行速度始终快于其他两个代码 - 您可以使用数组大小​​,但这是非常不重要的(可能是因为与其他方法相反,read(char[])不必获取锁定,因此每次调用的成本开始较低) .

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;
} 
Run Code Online (Sandbox Code Playgroud)

这很可能是性能良好的,但如果你想进一步使用文件映射来提高性能(在这种情况下不会指望太大的改进,但如果你知道你的文本总是ASCII ,你可以做一些进一步的优化)进一步帮助性能.

  • readCharTest应该是`readTest()`吗?(我将很快删除此评论) (2认同)