首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >为什么BufferedReader读取()比readLine()慢得多?

为什么BufferedReader读取()比readLine()慢得多?
EN

Stack Overflow用户
提问于 2016-12-25 20:12:23
回答 5查看 9.1K关注 0票数 44

我需要一次读取一个文件,并使用来自read()BufferedReader方法。*

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

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

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

代码语言:javascript
运行
复制
@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 ms:

代码语言:javascript
运行
复制
@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()方法。如果有更好的方法,请说。

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

EN

回答 5

Stack Overflow用户

回答已采纳

发布于 2016-12-25 22:32:19

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

我们必须考虑的一件事是,由于现代操作系统喜欢缓存定期访问的文件数据,所以我们需要某种方法来清除测试之间的缓存。在就这样上有一个小的实用程序--在Linux上,您应该可以通过将某个伪文件写入某个地方来完成它。

然后,代码看起来如下:

代码语言:javascript
运行
复制
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();
    }
}

如果我们用

代码语言:javascript
运行
复制
chcp 65001 # set codepage to utf-8
mvn clean install; java "-Dfile.encoding=UTF-8" -server -jar .\target\benchmarks.jar

我们得到了以下结果(为我清除缓存需要大约2秒,而我正在HDD上运行,这就是为什么它比您慢得多的原因):

代码语言:javascript
运行
复制
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方法中有一个异常值:

代码语言:javascript
运行
复制
# Warmup Iteration   1: 6.186 s/op
# Warmup Iteration   2: 3.744 s/op

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

取决于您的用例,这可能是一个大问题或可忽略不计的问题(如果您正在读取1000个文件,这将无关紧要,如果您只读取一个,这是一个问题)。

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

代码语言:javascript
运行
复制
# Warmup Iteration   1: 3.965 s/op
# Warmup Iteration   2: 3.753 s/op

可能的解决方案

现在我们已经很好地了解了实际的问题是什么(我的猜测仍然是那些锁既没有合并,也没有使用高效的有偏锁实现),解决方案非常简单:减少函数调用的数量(因此,如果没有上面的所有内容,我们就可以得到这个解决方案,但是能够很好地控制这个问题,并且可能有一个解决方案不需要修改太多的代码)。

下面的代码运行速度比其他两种方法都快--您可以使用数组大小,但这并不重要(可能是因为与其他方法相反,read(char[])不需要获得锁,因此每次调用的成本都较低)。

代码语言:javascript
运行
复制
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,您可以进一步优化)进一步提高性能。

票数 37
EN

Stack Overflow用户

发布于 2016-12-26 19:06:04

因此,这是我自己问题的实际答案:不要使用BufferedReader.read(),使用FileChannel。(很明显,我没有回答我为什么要写这个标题)。下面是快速而肮脏的基准,希望其他人会发现它有用:

代码语言:javascript
运行
复制
@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的字符读取整个文件字符,这种策略比BufferedReader.readLine() (~700 ms)要快得多,更不用说read()了。在循环中添加if语句以检查x == '\n'x == '>'没有什么区别。同时,放置一个StringBuilder来重建线条并不会对时间产生太大的影响。所以这对我有很多好处(至少现在是这样)。

感谢@Marco13 13提到FileChannel。

票数 3
EN

Stack Overflow用户

发布于 2016-12-25 20:49:05

Java优化了空循环体,因此您的循环实际上如下所示:

代码语言:javascript
运行
复制
while((c = fa.read()) != -1);

代码语言:javascript
运行
复制
while((line = fa.readLine()) != null);

我建议您阅读基准测试这里和循环这里的优化。

至于所需时间为何不同:

  • 原因一(只有当循环的主体包含代码时才适用):在第一个示例中,您在每一行执行一个操作,在第二个例子中,每个字符执行一个操作。这意味着你拥有的行/字符越多。 while((c = fa.read()) != -1){ //每个字符的一个操作。} while((line = fa.readLine()) != null){ //每行一个操作。}
  • 原因二:在类BufferedReader中,方法readLine()在幕后不使用read() --它使用自己的代码。readLine()方法每个字符读取一行的操作比用read()方法读取一行要少--这就是为什么readLine()读取整个文件的速度更快的原因。
  • 原因三:读取每个字符需要更多的迭代,而不是读取每一行(除非每个字符都在新行上);read()被调用的次数比readLine()多。
票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/41324192

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档