Monday, 21 September 2015

Chronicle-Bytes vs nio ByteBuffer: A worked example


Chronicle-Bytes is an Open Source project under the Chronicle group of technologies.

It has got some really interesting features and I would definitely recommend it if:
  • you find yourself at all frustrated with java.nio.ByteBuffer 
  • you are in low latency environment and want to avoid allocation
Some of the extensions over java.nio.ByteBuffer are explained in the README for the project.

This is a worked example comparing Chronicle-Bytes with the standard java.nio.ByteBuffer.

The main class in Chronicle-Bytes is  net.openhft.chronicle.bytes.Bytes it extends the functionality found in java.nio.ByteBuffer.

Let's see this in action with the example I introduced in this post where we saw how to prepend into Chronicle Bytes. 

This is the scenario:

You have a variable length message that needs to be written to a buffer. You need to prepend the message with its length so you know how far to read.  The length of that message needs to be written out to the buffer in text not binary (typical for FIX style message formats).

e.g.  The resulting buffer would look like this 11 hello world or 5 hello


Let's first look at the working this example through using java.nio.ByteBuffer.


Interspersed through the code we'll debug example Hello World in red.  We'll track the progress of the buffer through the program as follows:
  • hyphens - signify empty bytes, 
  • single | mark the position

//For the sake of this example we will use a StringBuilder with "Hello World"
//but assume this StringBuilder can be passed into a function and could contain
//any amount of text
StringBuilder sb = new StringBuilder("Hello World");
//For the sake of this example we have allocated a buffer of 20 bytes but in reality
//if the string buffer could be any size we would have to be careful how we sized
//the buffer.  This is one of the advantages of the elasticByteBuffer in Chronicle-Bytes
//which automatically resizes. 
ByteBuffer buffer = ByteBuffer.allocateDirect(20);
//Make sure the buffer is cleared from the last run - assuming this is called in a loop
bytes.clear();
|--------------------

//We need to convert the string into a byte array to add to the buffer later
//NOTE: This line will be a serious source of garbage
byte[] bytes = sb.toString().getBytes(StandardCharsets.UTF_8);
//Now work out the length which is the first thing to be written into the buffer
//Again in this line we will see garbage being produced.  
//Chronicle-Bytes has a direct method for writing numbers as text. 
buffer.put(Integer.toString(bytes.length).getBytes());
11|------------------
//Add a separator between the length and the message
buffer.put((byte) ' ');
11 |-----------------
//Add the bytes for the message
buffer.put(bytes);
11 Hello World|------
//Flipping is required to set into read mode.  You will see that in Chronicle-Bytes
//none of this is sort of thing is required because it has the concept of readPosition()
//and writePosition()
buffer.flip();
|11 Hello World------

//Now read the contents of the buffer
int number = 0;
//You have no option but to read the buffer byte by byte until you hit the
//space which is the end of the length.
byte b = buffer.get();
while(b >= '0' && b<='9'){
  number *= 10;
  number += b - '0';
  b = buffer.get();
}
11 |Hello World------
//We know how many bytes to read until we reach the end of the message
byte[] dst = new byte[number];
buffer.get(dst);
11 Hello World|------
//There is no way to read them directly into a String so we have no alternative
//but to create a new String for the bytes.
System.out.println(number + ":" + new String(dst, StandardCharsets.UTF_8));

This is the standard Java NIO way of working through the example.  It's slightly clunky in places but worst of all it's going to produce a lot of garbage.  As we know (and I've written about many times in this blog) garbage is the enemy of real time latencies.

Now let's work through the example with Chronicle-Bytes.

We will use the exactly the same example and follow the progress of the buffer as we step through the code. This time however we have both a readPosition and a writePosition:
  • hyphens - signify empty bytes, 
  • single | mark the write position 
  • double || mark the read position.
//For the sake of this example we will use a StringBuilder with "Hello World"
//but assume this StringBuilder can be passed into a function and could contain
//any amount of text
StringBuilder sb = new StringBuilder("Hello World");
//Using Bytes you don't need to worry about the length of your buffer - it will resize dynamically
//This really makes life easier.
Bytes bytes = Bytes.elasticByteBuffer();
|||--------------------
//Clear and pad allows you to leave room in your buffer so that you can write into the start of the
//buffer. For more information on prepending see here.
bytes.clearAndPad(8);
--------|||------------
//Bytes supports writing UTF8 from a CharSequence like a StringBuilder.  No need for
//any object creation here!
bytes.appendUtf8(sb);
--------||Hello World|-
//Effectively moves the readPosition backwards
bytes.prewriteByte((byte) ' ');
-------|| Hello World|-
//Bytes allows you write numbers directly as text!
bytes.prepend(sb.length());
-----||11 Hello World|-
//Note how there is no flip() required


//Bytes allows you to read text direct as a long - no object creation necessary
length = (int)bytes.parseLong();
-----11 ||Hello World|-
//Bytes allows you to read bytes straight into a CharSequence as UTF8
bytes.parseUTF(sb, length);
-----11 Hello World|||-
System.out.println(length + ":" + sb);


How do they Perform

I created a JMH benchmark to compare to compare Chronicle-Bytes with ByteBuffer (the full code listing for this is at the end of the post). 

As you can see from below Chronicle-Bytes runs almost twice as fast.

Benchmark                           Mode     Cnt    Score   Error  Units
BytesPerfTest.testByteBuffer      sample  451723  302.436 ± 8.260  ns/op

BytesPerfTest.testChronicleBytes  sample  418022  179.129 ± 1.307  ns/op

If you run the test with -verbosegc you will notice that whilst there are hundreds of collections triggered by testByteBuffer, testChronicleBytes produces triggers no collections at all.  This means that were you to account for coordinated omission in your tests the results would be far worse for ByteBuffer (for more details on coordinated omission see here).  

Conclusion

You should be able to see that Chronicle-Bytes is not only a great way to avoid object creation but is actually also simpler and more intuitive to use the the standard java.nio.ByteBuffer.

1 comment: