我需要一次读取一个文件,并且我正在使用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字符(n
或rn
)和剥离它们后的线长度。我还需要知道一行是否从>
字符开始。对于给定文件,这仅在程序开始时完成一次。由于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
命令时,也会发生类似的情况。