How fast can a BufferedReader read lines in Java?

In an earlier post, I asked how fast the getline function in C++ could run through the lines in a text file. The answer was about 2 GB/s, certainly over 1 GB/s. That is slower than some of the best disk drives and network connections. If you take into account that software rarely only need to “just” access the lines, it is easy to build a system where text-file processing is processor-bound, as opposed to disk or network bound.

What about Java? In Java, the standard way to access lines in a text file is to use a BufferedReader. To avoid system calls, I create a large string containing many lines of text, and then I call a very simple processing function that merely records the length of the strings…

StringReader fr = new StringReader(data);
BufferedReader bf = new BufferedReader(fr);
bf.lines().forEach(s -> parseLine(s));

// elsewhere:
public void parseLine(String s) {
  volume += s.length();
}

The result is that Java is at least two times slower than C++, on the same system, for this benchmark:

BufferedReader.lines 0.5 GB/s

This is not the best that Java can do: Java can ingest data much faster. However, my results suggest that on modern systems, Java file parsing might be frequently processor-bound, as opposed to system bound. That is, you can buy much better disks and network cards, and your system won’t go any faster. Unless, of course, you have really good Java engineers.

Many firms probably just throw more hardware at the problem.

My source code is available.

Update: An earlier version of the code had a small typo that would create just one giant line. This turns out not to impact the results too much. Some people asked for more technical details. I ran the benchmark on a Skylake processor using GNU GCC 8.1 as a C++ compiler and Java 12, all under Linux. Results will vary depending on your exact configuration.

Published by

Daniel Lemire

A computer science professor at the University of Quebec (TELUQ).

35 thoughts on “How fast can a BufferedReader read lines in Java?”

      1. BufferedReader internally uses a StringBuffer, which from my reading can be safely swapped out for a StringBuilder.

        I seem to get an improvement in performance from using a patched BufferedReader with that change.

        Code to be published later:

        Benchmark Mode Cnt Score Error Units
        MyBenchmark.stdLibBufferedReader thrpt 25 29.542 ± 0.599 ops/s
        MyBenchmark.patchedStdLibBufferedReader thrpt 25 33.426 ± 0.108 ops/s
        MyBenchmark.stringLines thrpt 25 87.141 ± 1.155 ops/s

        I’ll check the OpenJDK project to see whether that’s a reasonable change.

        1. Let me know if assistance is needed — the Java NIO Package is using BufferedReader as well and the Mutex locks in StringBuffer is causing nasty and unnecessary performance hits.

      1. The ideal buffer size and line size aren’t really related. The buffer size is for reading large(ish) chunks of the file which are then parsed into lines. Selecting the buffer size is mostly a function of system call overhead versus the desire to keep stuff in L1. I have repeatedly found 8K to be a sweet spot, although that was pre-Meltdown/Spectre which might have pushed the ideal buffer size up.

    1. You should try resizing the buffer to different sizes and rerun the benchmark. There is no magic behind buffered reader, everything is synchronous and it’s filling with data once the buffer is emptied.

  1. What’s the point of this post? Java is not easier to write [than C or C++], probably harder, one needs [to have installed] a VM and it’s slow, so what’s to like?

    1. What’s the point of this post? Java is not easier to write [than C or C++], probably harder, one needs [to have installed] a VM and it’s slow, so what’s to like?

      Java is much easier to write than C or C++ (for one, you don’t have to manually manage memory, and for second, it has much less corner points and nuances than C++), it’s plenty fast for most tasks (and on par with C/C++ on some), and installing a VM is a non issue.

      So you comment in wrong in each and every statement it makes…

      1. for one, you don’t have to manually manage memory, and for second, it has much less corner points and nuances than C++.

        Using RAII and smart pointers does away with manual memory management, forget C with classes, we moved on from there.

        Yes, it’s subtle and one needs to master it, it does not in itself mean it’s hard to write [and it got simpler to write fast code since C++1 and following std updates].

        I don’t need to ask my user to install the JVM, that seems like a major advantage.

      2. Forgot the most important bit, it is 2 times [with the optimizations in some of the other answers, 4 as posted] than plain C++. That’s the difference between google needing a mere 500’000 servers [to conduct its business] as compared to 1’000’000.

    2. What is the point of your comment? I get you don’t like Java, but it’s hardly relevant to the speed of I/O.

    3. Folks like you have been providing about 1/2 the work that I get, so THANKS! Java WAS slow, in the 90’s and early 2000’s. JIT around 2000 & the Runtime Profiler’s and Optimizers introduced around 2005 made Java quite performant. And there are lots of “extras” that the VM is providing if you are doing anything that needs Database, XML, WebServices, or any other non-trivial application.

  2. In contrast to the C getline function, BufferedReader.readLine and BufferedReader.lines does not include the newline character in the returned strings. It looks like you are building a huge one-lined string in scanFile, which would lead to repeated resizing of the read buffer later on.

    1. I played around with the code some more and the above suggestion does not really improve the performance as much as I thought. There is still too much copying of string contents happening.

      Using the indexOf/substring loop mentioned on hackernews gets the performance to about 2x the original, but substring is still creating copies. (This actually changed in java7, earlier it would create a view holding on to the original string contents which was deemed to be bad for memory consumption.)

      Using subsequence and changing parseLine to accept a CharSequence sounds like it should work, but behaves exactly the same as substring due to backwards compatibility, the subsequence method just delegates to substring.

      The one thing that gave a huge improvement was to implement a custom CharSequence implementation which does no copying and create that in an indexof loop. With that approach I finally got to about 2GB/s on this haswell laptop.

      So I completely agree with your point, java can be fast, but you’d have to know exactly what you’re doing. And often the standard library works against you.

      Modified code is available at https://gist.github.com/jhorstmann/9dcdc3c26a26e4ad6f513128942a47d9

  3. I’m Java illiterate, but there’s a comment on HN (https://news.ycombinator.com/item?id=20542438) that suggests you might not be measuring what you think you are measuring. Specifically, the author says that the call to lines() in your preprocessing step (L19) strips all the newlines, so that when you concatenate the results together with append() you are creating a single 23MB “line”. I’m not sure if it affects your conclusion, but given that your benchmarking is over a foreach loop, presumably this wasn’t your intent?

    It was also suggested (I think usefully) that a few more details about the test environment would be helpful to evaluate your result. While you mention it in the linked earlier post, it would probably help to say again which machine, which version of Java, which C++ compiler, and so forth so that the post is more standalone.

    1. you might not be measuring what you think you are measuring

      There was a typo in an earlier version of my code, but this was quickly corrected. It turns out not to affect the result… or, at least, not the conclusion.

      which version of Java, which C++ compiler, and so forth so that the
      post is more standalone.

      I’ll add more details but I think that this is somewhat nitpicking unless one can show that they consistently get 3 GB/s parsing text files in Java. That is, I provide an example that I view as ‘representative’ or ‘credible’.

  4. Have you tried?

    String s = null; while( (s = br.readLine()) != null) { parseLine(s); }

    I wonder how much overhead there in the streams.

  5. Please note that BufferedReader.lines is smarter than getline: it supports any line delimiters: ‘\n’, ‘\r’ or ‘\r\n’ while getline supports only ‘\n’. Clearly having more tests per each character adds some overhead. Though I would pay this overhead, rather than having garbage result if the input file comes from Windows. As it was mentioned above, use String.split(‘\n’) if you specifically need ‘\n’.

    1. Oh, sorry, String.split(‘\n’) was not mentioned above, and probably it’s not the best solution as it would allocate all the strings at once.

Leave a Reply

Your email address will not be published.

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax

You may subscribe to this blog by email.