Professional Documents
Culture Documents
Java Sequential IO Performance
Java Sequential IO Performance
dzone.com /articles/java-sequential-io-performance
2012-1-8
Many applications record a series of events to file-based storage for later use. This can be anything from
logging and auditing, through to keeping a transaction redo log in an event sourced design or its close relative
CQRS.
Java has a number of means by which a file can be sequentially written to, or read back again. This article explores
some of these mechanisms to understand their performance characteristics. For the scope of this article I will be
using pre-allocated files because I want to focus on performance. Constantly extending a file imposes a significant
performance overhead and adds jitter to an application resulting in highly variable latency. "Why is a pre-allocated
file better performance?", I hear you ask. Well, on disk a file is made up from a series of blocks/pages containing
the data. Firstly, it is important that these blocks are contiguous to provide fast sequential access. Secondly, meta-
data must be allocated to describe this file on disk and saved within the file-system. A typical large file will have a
number of "indirect" blocks allocated to describe the chain of data-blocks containing the file contents that make up
part of this meta-data. I'll leave it as an exercise for the reader, or maybe a later article, to explore the performance
impact of not preallocating the data files. If you have used a database you may have noticed that it preallocates the
files it will require.
The Test
I want to experiment with 2 file sizes. One that is sufficiently large to test sequential access, but can easily fit in the
file-system cache, and another that is much larger so that the cache subsystem is forced to retire pages so that new
ones can be loaded. For these two cases I'll use 400MB and 8GB respectively. I'll also loop over the files a number
of times to show the pre and post warm-up characteristics.
The tests are run on a 2.0Ghz Sandybridge CPU with 8GB RAM, an Intel 230 SSD on Fedora Core 15 64-bit Linux
with an ext4 file system, and Oracle JDK 1.6.0_30.
The Code
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.FileChannel;
System.gc();
long readDurationMs = testCase.test(PerfTestCase.Type.READ,
FILE_NAME);
out.format("%s\twrite=%,d\tread=%,d bytes/sec\n",
testCase.getName(),
bytesWrittenPerSec, bytesReadPerSec);
}
}
deleteFile(FILE_NAME);
}
file.close();
}
try
{
switch (type)
{
case WRITE:
{
checkSum = testWrite(fileName);
break;
}
case READ:
{
final int checkSum = testRead(fileName);
if (checkSum != this.checkSum)
{
final String msg = getName() +
" expected=" + this.checkSum +
" got=" + checkSum;
throw new IllegalStateException(msg);
}
break;
}
}
}
catch (Exception ex)
{
ex.printStackTrace();
}
buffer[pos++] = b;
if (PAGE_SIZE == pos)
{
file.write(buffer, 0, PAGE_SIZE);
pos = 0;
}
}
file.close();
return checkSum;
}
file.close();
return checkSum;
}
},
new PerfTestCase("BufferedStreamFile")
{
public int testWrite(final String fileName) throws Exception
{
int checkSum = 0;
OutputStream out =
new BufferedOutputStream(new FileOutputStream(fileName));
out.close();
return checkSum;
}
int b;
while (-1 != (b = in.read()))
{
checkSum += (byte)b;
}
in.close();
return checkSum;
}
},
new PerfTestCase("BufferedChannelFile")
{
public int testWrite(final String fileName) throws Exception
{
FileChannel channel =
new RandomAccessFile(fileName, "rw").getChannel();
ByteBuffer buffer = ByteBuffer.allocate(PAGE_SIZE);
int checkSum = 0;
if (!buffer.hasRemaining())
{
channel.write(buffer);
buffer.clear();
}
}
channel.close();
return checkSum;
}
while (buffer.hasRemaining())
{
checkSum += buffer.get();
}
buffer.clear();
}
return checkSum;
}
},
new PerfTestCase("MemoryMappedFile")
{
public int testWrite(final String fileName) throws Exception
{
FileChannel channel =
new RandomAccessFile(fileName, "rw").getChannel();
MappedByteBuffer buffer =
channel.map(READ_WRITE, 0,
Math.min(channel.size(), MAX_VALUE));
int checkSum = 0;
byte b = (byte)i;
checkSum += b;
buffer.put(b);
}
channel.close();
return checkSum;
}
checkSum += buffer.get();
}
channel.close();
return checkSum;
}
},
};
}
Results
400MB file
===========
RandomAccessFile write=379,610,750 read=1,452,482,269
bytes/sec
RandomAccessFile write=294,041,636 read=1,494,890,510
bytes/sec
RandomAccessFile write=250,980,392 read=1,422,222,222
bytes/sec
RandomAccessFile write=250,366,748 read=1,388,474,576
bytes/sec
RandomAccessFile write=260,394,151 read=1,422,222,222
bytes/sec
8GB File
============
RandomAccessFile write=167,402,321 read=251,922,012 bytes/sec
RandomAccessFile write=193,934,802 read=257,052,307 bytes/sec
RandomAccessFile write=192,948,159 read=248,460,768 bytes/sec
RandomAccessFile write=191,814,180 read=245,225,408 bytes/sec
RandomAccessFile write=190,635,762 read=275,315,073 bytes/sec
Analysis
For years I was a big fan of using RandomAccessFile directly because of the control it gives and the predictable
execution. I never found using buffered streams to be useful from a performance perspective and this still seems to
be the case.
In more recent testing I've found that using NIO FileChannel and ByteBuffer are the clear winners from a
performance perspective. With Java 7 the flexibility of this programming approach has been improved for random
access with SeekableByteChannel.
I've seen these results vary greatly depending on platform. File system, OS, storage devices, and available memory
all have a significant impact. In a few cases I've seen memory-mapped files perform significantly better than the
others but this needs to be tested on your platform because your mileage may vary...
A special note should be made for the use of memory-mapped large files when pushing for maximum throughput.
I've often found the OS can become unresponsive due the the pressure put on the virtual memory sub-system.
Conclusion
There is a significant difference in performance for the different means of doing sequential file IO from Java. Not all
8/9
methods are even remotely equal. For most IO I've found the use of ByteBuffers and Channels to be the best
optimised parts of the IO libraries. If buffered streams are your IO libraries of choice, then it is worth branching out
and and getting familiar with the sub-classes of Channel and Buffer.
From http://mechanical-sympathy.blogspot.com/2011/12/java-sequential-io-performance.html
9/9