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

2022-09-01 01:08:09

我需要一次读取一个字符的文件,并且我正在使用.*read()BufferedReader

我发现它比慢了大约10倍。这是意料之中的吗?还是我做错了什么?read()readLine()

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

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

@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

}

该方法仅需约 700 毫秒:readLine()

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

* 实际目的:我需要知道每行的长度,包括换行符(或)和剥离后的行长。我还需要知道一行是否以角色开头。对于给定文件,这仅在程序开始时执行一次。由于EOL字符不是由我返回的,所以我诉诸于该方法。如果有更好的方法可以做到这一点,请说。\n\r\n>BufferedReader.readLine()read()

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


答案 1

分析性能时,重要的是在开始之前有一个有效的基准测试。因此,让我们从一个简单的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();
    }
}

如果我们用

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

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

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

惊喜!正如预期的那样,在JVM稳定下来后,这里根本没有性能差异。但是在 readCharTest 方法中有一个异常值:

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

这就是你所看到的问题。我能想到的最可能的原因是OSR在这里做得不好,或者JIT只是运行得太晚,无法在第一次迭代中发挥作用。

根据您的用例,这可能是一个大问题或可以忽略不计(如果您正在读取一千个文件,那无关紧要,如果您只读取一个文件,这是一个问题)。

解决这样的问题并不容易,也没有通用的解决方案,尽管有办法解决这个问题。一个简单的测试是运行代码,该选项强制HotSpot在第一次调用时编译每个方法。实际上,这样做会导致第一次调用时的巨大延迟消失:-Xcomp

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

可能的解决方案

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

以下代码的运行速度始终比其他两个代码中的任何一个都快 - 您可以使用数组大小,但它令人惊讶地不重要(大概是因为与其他方法相反,不必获取锁,因此每次调用的成本较低)。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;
} 

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


答案 2

所以这是我自己问题的实际答案:不要使用使用代替。(显然,我没有回答我为什么在标题中加入)。这是快速而肮脏的基准测试,希望其他人会发现它很有用:BufferedReader.read()FileChannel

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

通过~250毫秒的时间来逐个读取整个文件char,这种策略比(~700 ms)快得多,更不用说了。在循环中添加 if 语句以进行检查,并且几乎没有区别。此外,放置 a 来重建线路不会对时间产生太大影响。所以这对我来说很好(至少现在是这样)。BufferedReader.readLine()read()x == '\n'x == '>'StringBuilder

感谢@Marco13提到FileChannel。


推荐