为什么bufferedReader read()比readline()慢得多



我需要一次读取一个文件,并且我正在使用BufferedReader中的read()方法。*

我发现read()readLine()慢约10倍。这是预期的吗?还是我做错了什么?

这是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
}

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
}

* 实用目的:我需要知道每行的长度,包括newline字符(nrn)和剥离它们后的线长度。我还需要知道一行是否从>字符开始。对于给定文件,这仅在程序开始时完成一次。由于BufferedReader.readLine()未返回EOL字符,因此我正在使用read()方法。如果有更好的方法,请说。

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

分析性能时重要的是要具有有效的基准测试你先来。因此,让我们从一个简单的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 .targetbenchmarks.jar

我们得到以下结果(需要大约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

惊喜!正如预期的那样,在JVM定居稳定模式后,这里根本没有性能差异。但是ReadChartest方法中有一个离群值:

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

这是您所看到的问题。我想到的最可能的原因是,OSR在这里做得不好,或者JIT的运行时间太晚,无法改变第一次迭代。

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

解决这样的问题并不容易,没有一般解决方案,尽管有一些方法可以处理。一个简单的测试可以查看我们是否在正确的轨道上是使用-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,您可以进行进一步的优化)进一步帮助性能。

,所以这是对我自己的问题的实用答案:不要使用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 ms读取CHAR的整个文件char,此策略比BufferedReader.readLine()(〜700毫秒)快得多,更不用说read()了。添加循环中的语句检查x == 'n'x == '>'是否没有什么区别。同样,将StringBuilder放置在重建线路上并不会太大影响时间。因此,这对我来说很有好处(至少目前)。

感谢 @marco13提到的Filechannel。

java jit优化空循环身体,因此您的循环实际上看起来像这样:

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

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

我建议您在此处阅读有关基准测试和循环的优化。


为何花时间有所不同:

  • 原因一(仅当循环包含代码的尸体时才适用):在第一个示例中,您每行进行一个操作,在第二个示例中,您是第二个示例每个角色做一个。这增加了您拥有的线/字符。

    while((c = fa.read()) != -1){
        //One operation per character.
    }
    while((line = fa.readLine()) != null){
        //One operation per line.
    }
    
  • 原因两个:在类BufferedReader中,方法readLine()在场景后面不使用read() - 它使用其自己的代码。与使用read()方法读取行所需的方法readLine()的每个字符的操作要少于读取行的操作 - 这就是为什么readLine()在读取整个文件时要快的速度。

  • 原因三:读取每个字符需要比读取每一行要多的迭代(除非每个字符在新行上);read()被称为比readLine()更多的次数。

感谢@voo的更正。我从FileReader#read() V/S BufferedReader#readLine()的角度进行了正确的观点,但从BufferedReader#read() V/S BufferedReader#readLine()观点不正确,所以我已经删除了答案。

BufferedReader上使用read()方法不是一个好主意,它不会造成任何伤害,但它肯定会浪费班级的目的。

BufferedReader生活中的全部目的是通过缓冲内容来减少I/O。您可以在Java教程中阅读。您可能还会注意到,BufferedReader中的read()方法实际上是从Reader继承的,而readLine()BufferedReader的方法。

如果您想使用read()方法,那么我会说您最好使用FileReader,这是为此目的的。您可以在此处阅读Java教程。

so,我认为回答您的问题非常简单(不进入台式标记和所有解释) -

  • 每个read()都是通过基础操作系统和触发磁盘访问,网络活动或其他相对昂贵的操作来处理的。
  • 当您使用readLine()时,您可以保存所有这些开销,因此readLine()始终比read()更快,对于小数据而言可能不是基本上的。

如果您考虑一下这种差异也就不足为奇了。一个测试是迭代文本文件中的行,而另一个是迭代字符。

除非每行包含一个字符,否则预计readLine()的速度比read()方法快。(尽管如上所述所指出的那样,它是可说的,因为BufferedReader buffers buffers the Input the Input the Input,而物理文件读取可能不是唯一的表演操作)

如果您真的想测试2之间的差异,我建议您在两个测试中迭代每个字符的设置。例如。类似:

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

除上述内容外,请使用" Java性能诊断工具"来对您的代码进行基准测试。另外,根据文档:

根据文档:

每个read()调用都会使系统调用昂贵。

每个readLine()调用仍然可以一次昂贵的系统调用,但是,一次单元一次,因此呼叫较少。

当我们为要更新的每个记录进行数据库update命令时,也会发生类似的情况。

相关内容

  • 没有找到相关文章

最新更新