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

时间:2016-12-25 20:12:23

标签: java bufferedreader benchmarking

我需要一次读取一个字符,我正在使用read()中的BufferedReader方法。 *

我发现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
}

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

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

6 个答案:

答案 0 :(得分:35)

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

我们必须考虑的一件事是,由于现代操作系统喜欢缓存定期访问的文件数据,我们需要一些方法来清除测试之间的缓存。在Windows上有一个小小的实用程序that does just this - 在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秒才能清除我的缓存,并且我在硬盘上运行此缓存,这就是为什么它比你的速度慢得多) :

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选项的代码,该选项迫使HotSpot在第一次调用时编译每个方法。确实这样做会导致第一次调用的大延迟消失:

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

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

答案 1 :(得分: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
}

通过char读取整个文件char约250毫秒,这个策略比BufferedReader.readLine()(~700毫秒)快得多,更不用说read()了。在循环中添加if语句以检查x == '\n'x == '>'几乎没有区别。同时使用StringBuilder重建行不会对时间造成太大影响。所以这对我来说是好事(至少目前为止)。

感谢@ Marco13提及FileChannel。

答案 2 :(得分:1)

感谢@Voo进行更正。从FileReader#read() v / s BufferedReader#readLine()的观点来看,我在下面提到的是正确的但是从BufferedReader#read() v / s BufferedReader#readLine()的观点来看不正确,所以我已经罢工了答案。

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

<击>

BufferedReader生活中的一个目的是通过缓冲内容来减少i / o。您可以在Java教程中阅读here。您可能还注意到,read()中的BufferedReader方法实际上是从Reader继承而readLine()BufferedReader自己的方法。

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

所以,我觉得你的问题答案非常简单(没有进行基准测试和所有解释) -

    <击>
  • 每个read()由底层操作系统处理,并触发磁盘访问,网络活动或其他相对昂贵的操作。
  • 当您使用readLine()时,您可以节省所有这些费用,因此readLine()总是比read()快,但可能不会对小数据产生影响,更快。

答案 3 :(得分:0)

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

除非每行包含一个字符,否则readLine()read()方法更快。(尽管如上面的注释所指出的那样,因为BufferedReader缓冲了它,所以它是有争议的。输入,而物理文件读取可能不是唯一的性能获取操作)

如果你真的想测试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));
}

除上述内容外,请使用&#34; Java性能诊断工具&#34;对代码进行基准测试另外,请在how to microbenchmark java code上阅读。

答案 4 :(得分:0)

Java JIT优化了空循环体,所以你的循环实际上是这样的:

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

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

我建议您阅读基准here和优化循环here

至于为什么花费的时间不同:

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

    while((c = fa.read()) != -1){
        //One operation per character.
    }
    
    while((line = fa.readLine()) != null){
        //One operation per line.
    }
    
  • 原因二:在班级BufferedReader中,方法readLine()在幕后不使用read() - 它使用自己的代码。方法readLine()每个字符读取一行的操作少于使用read()方法读取一行所需的操作 - 这就是readLine()读取整个文件的速度更快的原因。

  • 原因三:读取每个字符需要更多次迭代,而不是读取每一行(除非每个字符都在新行上); read()被调用的次数超过readLine()

答案 5 :(得分:0)

根据文件:

每次read()方法调用都会进行昂贵的系统调用。

每次readLine()方法调用仍会进行昂贵的系统调用,但是,一次调用更多字节,因此调用次数更少。

当我们为要更新的每条记录创建数据库update命令而不是批量更新时,会发生类似情况,我们会对所有记录进行一次调用。