为啥 BufferedReader read() 比 readLine() 慢得多?

Posted

技术标签:

【中文标题】为啥 BufferedReader read() 比 readLine() 慢得多?【英文标题】:Why is BufferedReader read() much slower than readLine()?为什么 BufferedReader read() 比 readLine() 慢得多? 【发布时间】:2017-05-10 12:03:07 【问题描述】:

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

我发现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 文件。

【问题讨论】:

请阅读如何编写准确的 Java 基准测试。 @Louis Wasserman 诚然,我并不太关心我的基准测试是否准确。 JUnit 和 currentTimeMillis() 并不理想,但我认为在一个相当大的文件上 8-10 倍的时间差足以提出这个问题。 @dariober 你最好使用public int read(char[] cbuf, int off, int len) throws IOException 而不是直接使用bufferdreader 的read 函数。最终,您的目标是在文件中找到行尾。虽然我自己没有测试过,但是控制在你手中的缓冲区可能会给你带来更好的结果。 快速检查后:测试可能(!)不仅有缺陷,我认为它完全有缺陷。尝试在read 测试之前运行readLine 测试,看看时间是否不同。这可能只是与 HDD 缓存或 JIT 有关(对我来说,旧的慢速 HDD 在第一次运行时的时间差是 1:7,但在后续运行中大约是 1:2。所以事实上,请尝试运行 @987654340 @ 并告诉我们结果...) @Marco13 尝试在读取测试之前运行 readLine 测试:我在 Eclipse 中执行此操作,我尝试了几次关闭 Eclipse 并重新打开它(是这足以清除缓存和所有内容?)。我也使用nanoTime() 而不是currentTimeInMillis()。我发现即使我先运行 readLine,结果也几乎相同。 (比如 readLine() 与 read() 的 1:6)。我正在使用带 SSD 的 Mac 笔记本电脑。 【参考方案1】:

根据文档:

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

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

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

【讨论】:

【参考方案2】:

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

除非每一行包含一个字符,否则 readLine() 预计比 read() 方法快得多。(尽管正如上面的 cmets 所指出的,这是有争议的,因为 BufferedReader 缓冲输入,而物理文件读取可能不是唯一占用性能的操作)

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

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 性能诊断工具”对您的代码进行基准测试。另外,阅读how to microbenchmark java code。

【讨论】:

这并不是一个真正的微基准测试。海报的方法,无论多么原始,对于所涉及的时间尺度和时间比率来说并不是不合理的。您可以对此使用 unix time 命令充满信心,您会看到显着的效果。【参考方案3】:

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

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

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

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

所以,我认为您的问题的答案非常简单(无需进行基准测试和所有解释)-

每个read() 都由底层操作系统处理并触发磁盘访问、网络活动或其他一些相对昂贵的操作。 当您使用readLine() 时,您可以节省所有这些开销,因此readLine() 将始终比read() 快,对于小数据而言可能不是很大,但速度更快。

【讨论】:

正如 cmets 中已经提到的:Buffered (!) 阅读器背后的目标是它缓冲一些数据。因此,重复的read() 调用将不会导致从磁盘中逐个读取字节。相反,它会定期读取“块”数据。您甚至可以追溯它以查看 readreadLine 方法中的两者,底层 FileReader 正在执行相同的 read 调用,每个读取 8192 字节。 @Marco13 这篇文章中有很多 cmets,我什至没有阅读一些,但我确实阅读了答案。如果您的观点是 read 也做一些缓冲,那么我不确定,但我不能排除可能会有一些优化,但关于 BufferedReaderFileReader 类的目的的基本知识仍然相同,并且为什么 readreadLine 慢 - 因为涉及更多的 i/o。 @hagrawal 您实际上可以通过查看文档的第一段(或快速浏览代码)非常容易地排除这种情况。尽管名称本身似乎是一个死的赠品 - 如果 BufferedReader 不缓冲读取,它还能做什么? @Voo 看起来你要么误读了我的回答,要么误解了我的回答,你能指出是哪一部分让你写了这个 - “如果 BufferedReader 不缓冲读取,它还能做什么? i>”,我想了解您认为我在哪里试图传达 BufferedReader 没有缓冲,然后最终让您对我的回答投了反对票! @hagrawal “每个 read() 都由底层操作系统处理并触发磁盘访问、网络活动或其他一些相对昂贵的操作”。这不是真的。如果您每 8000 字节使用一个缓冲读取器,将导致一次内核调用。但是对于读取行也是如此。 readLine 避免的唯一开销是多次 read() 调用来读取并将它们包含在一个中(从而避免重复获取锁等)【参考方案4】:

所以这是我自己的问题的实用答案:不要使用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 毫秒来逐个字符地读取整个文件,这个策略比BufferedReader.readLine() (~700 ms) 快得多,更不用说read()了。在循环中添加 if 语句以检查 x == '\n'x == '&gt;' 几乎没有区别。此外,将StringBuilder 用于重建线路不会对时间产生太大影响。所以这对我来说很好(至少现在是这样)。

感谢 @Marco13 提到 FileChannel。

【讨论】:

【参考方案5】:

分析性能时,重要的是在开始之前有一个有效的基准。因此,让我们从一个简单的 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 秒,我在 HDD 上运行它,所以它比你慢很多):

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 进一步提高性能可能(在这种情况下不会指望太大的改进,但是如果您知道您的text 总是 ASCII,你可以做一些进一步的优化)进一步帮助性能。

【讨论】:

readCharTest 应该是 readTest() 吗? (我会尽快删除此评论) 好消息!我能够重现您的结果,但我认为引入的噪音使它们在很大程度上无效 - 您正在测量缓存清除并且您添加了非等效处理,这压倒了实际测量的东西。我有两个普遍的批评 - 一个是(更“基于意见”的一个)是这不是一个真正的微基准,所以方法本身是不具代表性的。另一个问题是,即使我们接受该方法,也不难得出 50% 和 300% 之间的性能差异 - 即这些具体测量值不具代表性。 我明天会尝试写下我的结果并发布。 @Marco13 所以简单的答案是,虽然 最终 jvm 可以使读取版本相当快,但由于所有显而易见的原因,它的效率非常低。在第一次运行时,每个人的数字都对性能造成了巨大的影响。所以不要使用它。 @Voo 当我删除您的处理时,readline 即使在重复时也会始终获胜,并且随着数据大小的增加,它会获胜更多。当然,它在一次性情况下完全破坏了read,在我看来,这实际上代表了实际用途。您是否认为您在答案中所写的内容 - “分析性能时重要的是要有一个有效的基准”与给出的结论相匹配(“read 和 readline 执行相同”)。对我来说,无论是基准还是数据(我们似乎至少在数据的方差上达成一致)都是无效的。【参考方案6】:

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() 多。

【讨论】:

如果java优化掉这些循环,就不会有时间差异了。 @pvg 请查看编辑。 readreadLine 读取文件的方式不同。他们仍然在循环中被调用。 我认为空循环并不重要。我将if(line.contains("&gt;")) System.out.println(line); 放在readLine() 测试的循环中,将if(c == '&gt;') System.out.println(c); ; 放在read() 中。结果保持不变。 我阅读了编辑。这句话 - '另一件事,你的基准完全没有。 Java 优化掉空循环。 '基本上是错误的。这些循环有副作用并且没有被优化掉。基准是粗略但合理的。 我认为您需要更加小心术语。说一个循环被“优化掉”通常意味着整个循环——包括终止条件检查。 OP 的问题中的代码有一个空的主体,但循环有一个副作用,如果不改变代码的语义就无法优化。说“循环体被优化掉”是没有意义的,因为循环体一开始是空的;没有什么可以优化的。

以上是关于为啥 BufferedReader read() 比 readLine() 慢得多?的主要内容,如果未能解决你的问题,请参考以下文章

java中关于bufferedreader类中read方法

关于java中BufferedReader的read()及readLine()方法的使用心得

FileReader 和 BufferedReader 的作用——为啥要包装 FileReader?

bufferedreaderreadline换行符不完全卡死

Java,为啥从 MappedByteBuffer 读取比从 BufferedReader 读取慢

IO流之BufferedReader/BufferedWriter