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

我需要一次读取一个字符的文件,并使用BufferedReaderread()方法。 *

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

这是Java 7的基准testing。inputtesting文件有大约500万行和254万字符(〜242 MB)**:

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

 @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()方法只需要readLine() ms:

 @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 )和剥离后的行长。 我还需要知道一行是否以>字符开头。 对于给定的文件,这只在程序开始时执行一次。 由于EOL字符不是由BufferedReader.readLine()返回我正在使用read()方法。 如果有更好的方法,请说。

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

分析性能时最重要的事情就是在开始之前有一个有效的基准。 那么让我们从一个简单的JMH基准开始,它显示了我们预热后的预期性能。

我们必须考虑的一件事是,由于现代操作系统喜欢caching经常访问的文件数据,所以我们需要一些方法来清除testing之间的caching。 在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秒来清除caching对我来说,我在硬盘上运行,所以这是为什么它比你慢很多):

 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方法有一个exception点:

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

这是你看到的问题的可能性。 我能想到的最可能的原因是OSR在这里做得不好,或者JIT只是跑得太晚而不能在第一次迭代中做出改变。

根据您的使用情况,这可能是一个大问题或可以忽略不计(如果您正在阅读一千个文件,这将无所谓,如果你只读一个这是一个问题)。

解决这样的问题并不容易,也没有一般的解决scheme,尽pipe有办法解决这个问题。 一个简单的testing,看看我们是否在正确的轨道上运行的代码与-Xcomp选项迫使HotSpot编译第一次调用的每个方法。 而且确实这样做会导致第一次调用的大的延迟消失:

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

可能的scheme

现在我们有一个好主意是什么实际问题是(我的猜测仍然是所有这些锁既不合并,也没有使用有效的偏见锁实现),解决scheme是非常简单和简单的:减less函数调用的数量(所以是我们可以在没有上述任何条件的情况下达到这个解决scheme,但是总是能够很好地处理这个问题,并且可能有一个解决scheme不涉及更改代码)。

下面的代码一直运行的速度比其他任何一个都快 – 你可以玩数组大小,但它是惊人的不重要(可能是因为与其他方法相反, 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 ,你可以进一步优化)进一步帮助性能。

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

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

BufferedReader在生活中的全部目的是通过缓冲内容来减lessI / O。 你可以在这里阅读Java教程。 你也可能注意到BufferedReader中的read()方法实际上是从Readerinheritance的,而readLine()BufferedReader自己的方法。

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

所以, 我认为回答你的问题是非常简单的 (没有进入基准testing和所有的解释)

  • 每个read()都由底层操作系统处理,并触发磁盘访问,networking活动或其他相对昂贵的操作。
  • 当你使用readLine()那么你保存所有这些开销,所以readLine()将总是比read()快,可能不是小数据,但更快。

所以这是我自己的问题的实际答案:不要使用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 ms,这个策略比BufferedReader.readLine() (〜700 ms)快很多,更不用说read() 。 在循环中添加if语句以检查x == '\n'x == '>'没有什么区别。 另外把一个StringBuilder重构线不会影响太多的时间。 所以这对我来说是很好的(至less现在是这样)。

感谢@ Marco13提及FileChannel。

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

除非每行都包含一个字符,否则预期readLine()read()方法快(尽pipe正如上面的注释所指出的那样,由于BufferedReader缓冲input,而物理文件读取可能不是唯一的表演服务)

如果你真的想testing两者之间的差异,我会build议在两个testing中迭代每个字符的设置。 例如:

 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性能诊断工具”来testing您的代码。 另外,请阅读关于如何微调Java代码的基础知识 。

Java JIT优化了空循环体,所以你的循环看起来像这样:

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

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

我build议你阅读这里的基准和这里的循环优化。


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

  • 原因之一(这只适用于循环的主体包含代码):在第一个例子中,你每行做一个操作,在第二个,你做每个字符一个。 这增加了你有更多的线/字符。

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

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