In the last blog we looked at how Red Hat’s engineers have lead the work evolving OpenJDK to provide efficient access to Persistent Memory.
In this post we get hands-on, showing how to use the new API to write code with JDK 14
A MappedByteBuffer provides a way to access a region of memory, corresponding to a file, from Java.
The steps to creating a MappedByteBuffer for Persistent Memory are almost the same as in the non-persistent case. First, create a FileChannel:
final File file = new File("/path/to/some/file");
final FileChannel fileChannel = (FileChannel) Files
.newByteChannel(file.toPath(), EnumSet.of(
StandardOpenOption.READ,
StandardOpenOption.WRITE,
StandardOpenOption.CREATE));
MappedByteBuffer mappedByteBuffer = null;
Next, at the point the memory map is instantiated, use the new ExtendedMapMode flag to take advantage of DAX mode mapping:
try {
mappedByteBuffer = fileChannel.map(ExtendedMapMode.READ_WRITE_SYNC, position, size);
} catch(IOException e) {
// mapping failed! maybe retry with MapMode.READ_WRITE instead?
}
Note that the DAX mode mapping will fail with IOException: Operation not supported if the underlying file is not on a DAX enabled filesystem. If that happens, you can simply fallback to a regular mapping.
That’s it! No further code changes are required to enable use of the optimizations.
The newly created MappedByteBuffer instance will internally carry a flag indicating that it is DAX enabled.
To guarantee persistence of changes to the buffer, just call mappedByteBuffer.force() as normal.
The new JDK library implementation of force() will consult the flag and use performance optimized cache management instructions if it can, or fallback to the old behaviour of calling msync() if it can’t.
But wait, there is more!
There is one further optional change you can make to improve write performance still further.
The MappedByteBuffer API has also been updated to offer a new force(int index, int length) variant.
For the non-optimized case this provides little gain, as the O/S already tracks dirty blocks and flushes only those that have been modified when force() calls msync()
For the optimized case however, there is no dirty tracking in software, so the JDK will issue cache writeback instructions for each cache line in the entire range.
Although the cache hardware can optimize out the writebacks for non-dirty or non-loaded lines, it’s still an overhead to do so. Better not to issue the instructions in the first place.
Hence the finer grained version of force() that allows a range to be provided explicitly.
The optimizations are particularly powerful for applications that require small log append operations or other modifications of sub-block size, as the I/O is now at cache line (64 byte) granularity rather than block (4K) granularity.