Buffered I/O and Performance in Java

Every unbuffered read is a system call. Buffering turns thousands of them into a handful, and it is the single largest I/O win available.

Why buffering matters

An unbuffered stream asks the operating system for data on every call. A buffered one asks for a large block, keeps it in memory, and serves individual reads from there. The difference is not a small percentage; it is often an order of magnitude.

Unbuffered: read() -> system call -> 1 byte      x 100000 times
Buffered:   read() -> system call -> 8192 bytes  x 13 times, rest from memory

The difference in code

// Slow: one system call per byte
try (InputStream in = Files.newInputStream(path)) {
    int b;
    while ((b = in.read()) != -1) {
        process(b);
    }
}

// Fast: the same loop, one wrapper added
try (InputStream in = new BufferedInputStream(Files.newInputStream(path))) {
    int b;
    while ((b = in.read()) != -1) {
        process(b);
    }
}

The buffered classes

ClassWrapsAdds
BufferedInputStreamInputStreamA byte buffer
BufferedOutputStreamOutputStreamA byte buffer
BufferedReaderReaderA char buffer and readLine
BufferedWriterWriterA char buffer and newLine
BufferedReader reader = new BufferedReader(new FileReader(file), 65536);   // custom size

The default buffer is 8192 units. A larger buffer helps for very large sequential files, but the gain flattens quickly, so measure before tuning.

readLine comes from buffering

try (BufferedReader reader = Files.newBufferedReader(path)) {
    String line;
    while ((line = reader.readLine()) != null) {
        process(line);
    }
}

A plain Reader has no readLine. Finding a line break requires looking ahead, which needs a buffer, so the method lives on BufferedReader.

Flushing

BufferedWriter writer = Files.newBufferedWriter(path);
writer.write("important");
// the file may still be empty here: the data is in the buffer

writer.flush();     // force it out
writer.close();     // flushes and then closes
// try with resources closes, and therefore flushes, on every path
try (BufferedWriter writer = Files.newBufferedWriter(path)) {
    writer.write("important");
}
Data lost because a writer was never closed is one of the most common I/O bugs. try with resources removes it entirely, which is reason enough to use it every time.

Bulk transfer beats a loop

// Good
try (InputStream in = new BufferedInputStream(Files.newInputStream(source));
     OutputStream out = new BufferedOutputStream(Files.newOutputStream(target))) {

    byte[] buffer = new byte[8192];
    int read;
    while ((read = in.read(buffer)) != -1) {
        out.write(buffer, 0, read);
    }
}

// Better
try (InputStream in = Files.newInputStream(source);
     OutputStream out = Files.newOutputStream(target)) {
    in.transferTo(out);
}

// Best for a plain file copy
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);

Note out.write(buffer, 0, read). Writing the whole array would append rubbish from the previous iteration on the final, partial read.

Which Files method already buffers

MethodBuffered
Files.newBufferedReader, newBufferedWriterYes
Files.linesYes
Files.readString, readAllLines, readAllBytesReads in bulk, so buffering is irrelevant
Files.newInputStream, newOutputStreamNo, wrap it yourself
Files.copyYes, internally

Memory against speed

// Fast but holds the whole file in memory
List<String> all = Files.readAllLines(path);

// Constant memory, suitable for any size
try (Stream<String> lines = Files.lines(path)) {
    lines.filter(this::isRelevant).forEach(this::process);
}

Choose by file size. Loading a configuration file entirely is sensible; loading a multi gigabyte log is not.

Random access

try (RandomAccessFile file = new RandomAccessFile("data.bin", "r")) {
    file.seek(1024);
    int value = file.readInt();
}

try (FileChannel channel = FileChannel.open(path, StandardOpenOption.READ)) {
    ByteBuffer buffer = ByteBuffer.allocate(1024);
    channel.read(buffer, 4096);          // read at a position
}

Streams are sequential. When arbitrary positions are needed, use RandomAccessFile or a FileChannel.

A practical benchmark shape

long start = System.nanoTime();
long lines = 0;
try (BufferedReader reader = Files.newBufferedReader(path)) {
    while (reader.readLine() != null) {
        lines++;
    }
}
System.out.printf("%d lines in %d ms%n", lines, (System.nanoTime() - start) / 1_000_000);

Run the same file through a buffered and an unbuffered reader and the difference speaks for itself. Remember that the operating system also caches, so a second run of the same file is always faster.

Common mistakes

  • Reading or writing without buffering.
  • Not closing a writer, and losing everything still in the buffer.
  • Writing the whole array rather than write(buffer, 0, read).
  • Wrapping a BufferedReader around another one, which adds a layer for nothing.
  • Loading a huge file with readAllLines.
  • Calling flush after every small write, which defeats the buffer.

Best practices

  • Buffer every stream, or use a Files method that already does.
  • Always use try with resources.
  • Prefer transferTo or Files.copy to a hand written copy loop.
  • Stream large files instead of loading them.
  • Leave the default buffer size unless measurement shows otherwise.
  • Flush only when another process must see the data before the stream closes.

Practice

  1. Time a buffered and an unbuffered read of the same large file and report the ratio.
  2. Write to a file without closing the writer and explain what the file contains.
  3. Why does readLine exist on BufferedReader rather than on Reader?
  4. Explain the bug in out.write(buffer) inside a copy loop.
  5. Choose between readAllLines and Files.lines for a 4 GB log, and justify it.

Conclusion

Buffering converts a system call per byte into a system call per block, and it costs one wrapper. Buffer everything, close everything with try with resources, and stream rather than load when the file is large.

Written by Lorens Mishra

Default administrator account created by the installer.

Continue reading

All Java notes →
Java

Introduction to Java

Java is a statically typed, object oriented language that compiles to bytecode and runs on a virtual machine, which is what makes it portable.

Read more

Discussion

0 comments
Sign in to join the discussion.

No comments yet. Be the first to say something.