Testing Performance Of Various Byte Array / Binary Value Generation Methods In ColdFusion
The other day, I experimented with Java's ByteBuffer class as a means to generate a byte array / binary value in ColdFusion. In the past, I've used a whole host of techniques for creating byte arrays; but, I was terribly intrigued by the fact that ByteBuffer seemed so "close to the metal". Meaning, it appeared to be nothing more than a thin layer on top of the actual binary memory allocation. This got me thinking about performance and I wanted to see how the use of ByteBuffer compared to other techniques that I've used.
First, I wanted to confirm that all the techniques being tested actually generated the same binary value / byte array in ColdFusion. So, I generated small test values and then compared their HEX-encoded output:
<cfscript>
a = javaCast( "byte[]", [ 32, 32, 32, 32, 32 ] );
b = charsetDecode( repeatString( chr( 32 ), 5 ), "utf8" );
c = createObject( "java", "java.nio.ByteBuffer" )
.allocate( javaCast( "int", 5 ) )
.put( javaCast( "byte", 32 ) )
.put( javaCast( "byte", 32 ) )
.put( javaCast( "byte", 32 ) )
.put( javaCast( "byte", 32 ) )
.put( javaCast( "byte", 32 ) )
.array()
;
dStream = createObject( "java", "java.io.ByteArrayOutputStream" )
.init( javaCast( "int", 5 ) )
;
dStream.write( javaCast( "int", 32 ) );
dStream.write( javaCast( "int", 32 ) );
dStream.write( javaCast( "int", 32 ) );
dStream.write( javaCast( "int", 32 ) );
dStream.write( javaCast( "int", 32 ) );
d = dStream.toByteArray();
e = repeatString( chr( 32 ), 5 ).getBytes();
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// Convert the values to HEX so we can easily compare them.
aHex = binaryEncode( a, "hex" );
bHex = binaryEncode( b, "hex" );
cHex = binaryEncode( c, "hex" );
dHex = binaryEncode( d, "hex" );
eHex = binaryEncode( e, "hex" );
writeOutput( "a: #aHex#<br />" );
writeOutput( "b: #bHex#<br />" );
writeOutput( "c: #cHex#<br />" );
writeOutput( "d: #cHex#<br />" );
writeOutput( "e: #eHex#<br />" );
// Test equality through substitution principle.
if ( ( aHex == bHex ) && ( bHex == cHex ) && ( cHex == dHex ) && ( dHex == eHex ) ) {
writeOutput( "All values are equal" );
} else {
writeOutput( "Something went wrong!" );
}
</cfscript>
As you can see, each byte array in this test is of length 5 and is filled with the first-byte representation of the integer 32 (which is also the decimal value for the ACSII character Space). When we run this code, we get the following output:
a: 2020202020
b: 2020202020
c: 2020202020
d: 2020202020
e: 2020202020
All values are equal
Excellent - all the approaches I'm about to test for performance provide the same output.
Before we look at performance, though, let me just talk about why I used "32" in the above test. Ideally, I would have liked to just fill each binary value with the zero byte (0). But, the problem is that:
repeatString( chr( 0 ), 5 )
... doesn't do what you might expect. It actually returns an empty string. As such, I can't use the repeatString() approach to create a zero-filled byte array. But, I think that's OK for many cases. In ColdFusion, when you create a byte array / binary value, it's often times just an intermediary buffer into which binary data is going to be written. As such, the initial values in the byte array don't matter - they get overwritten as part of their workflow. That's why my performance test below uses a Space in some of the perf tests.
That said, let's look at some simplistic performance measurements: how many times can I generate a byte array / binary value in a given amount of time:
<cfscript>
// NOTE: This is here as a global variable for one of the tests.
ByteBuffer = createObject( "java", "java.nio.ByteBuffer" );
// Test each method to see how many iterations can be run in the same duration.
writeOutput( "ByteBuffer: #numberFormat( runTest( testByteBuffer, 2000 ) )#<br />" );
writeOutput( "ByteBuffer2: #numberFormat( runTest( testByteBuffer2, 2000 ) )#<br />" );
writeOutput( "Manual Build: #numberFormat( runTest( testManualBuild, 2000 ) )#<br />" );
writeOutput( "Repeat String: #numberFormat( runTest( testRepeatString, 2000 ) )#<br />" );
writeOutput( "Repeat String2: #numberFormat( runTest( testRepeatString2, 2000 ) )#<br />" );
writeOutput( "Output Stream: #numberFormat( runTest( testByteArrayOutputStream, 2000 ) )#<br />" );
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// I test the ByteArrayOutputStream by writing individual bytes to the stream.
public void function testByteArrayOutputStream() {
var stream = createObject( "java", "java.io.ByteArrayOutputStream" )
.init( javaCast( "int", 1024 ) )
;
for ( var i = 1 ; i <= 1024 ; i++ ) {
stream.write( javaCast( "int", 0 ) );
}
var buffer = stream.toByteArray();
}
// I test the ByteBuffer by creating the ByteBuffer static class each time.
public void function testByteBuffer() {
var buffer = createObject( "java", "java.nio.ByteBuffer" )
.allocate( javaCast( "int", 1024 ) )
.array()
;
}
// I test the ByteBuffer by using a cached instance of the ByteBuffer class.
// --
// NOTE: This is a more realistic test as the Java class would likely be cached
// inside whichever ColdFusion component was generating the binary values.
public void function testByteBuffer2() {
var buffer = ByteBuffer
.allocate( javaCast( "int", 1024 ) )
.array()
;
}
// I test the manual building of individual bytes.
public void function testManualBuild() {
var bytes = [];
arrayResize( bytes, 1024 );
arraySet( bytes, 1, 1024, 0 );
var buffer = javaCast( "byte[]", bytes );
}
// I test the string-to-bytes approach.
// --
// CAUTION: We are not actually creating an equivalent buffer here since this one
// will be filled with the byte 32 (space). That said, I'm including this as part
// of the test because this is completely fine in cases where all you need is an
// intermediary buffer into which you will write bytes and then read those bytes.
// In those cases, the initial value of the bytes in the buffer don't matter.
public void function testRepeatString() {
var buffer = charsetDecode( repeatString( " ", 1024 ), "utf8" );
}
// I test the string-to-bytes approach using the hidden Java method, getBytes().
// --
// CAUTION: We are not actually creating an equivalent buffer here since this one
// will be filled with the byte 32 (space). That said, I'm including this as part
// of the test because this is completely fine in cases where all you need is an
// intermediary buffer into which you will write bytes and then read those bytes.
// In those cases, the initial value of the bytes in the buffer don't matter.
public void function testRepeatString2() {
var buffer = repeatString( " ", 1024 ).getBytes();
}
// ------------------------------------------------------------------------------- //
// ------------------------------------------------------------------------------- //
// I run the given callback as many times as I can in the given duration and return
// the iteration count.
public numeric function runTest(
required any callback,
required numeric duration
) {
var targetTick = ( getTickCount() + duration );
var count = 0;
while ( getTickCount() < targetTick ) {
count++;
callback();
}
return( count );
}
</cfscript>
As you can see, each test has 2-seconds to run as many times as it can. And, when we run the above code a few times, we get the following output:
NOTE: I am altering the output to list in order of performance.
Repeat String: 1,521,846
ByteBuffer2: 1,499,936
ByteBuffer: 1,056,772
Repeat String2: 252,411
Manual Build: 24,430
Output Stream: 6,086ByteBuffer2: 1,650,297
Repeat String: 1,471,474
ByteBuffer: 1,057,972
Repeat String2: 256,842
Manual Build: 25,372
Output Stream: 5,720ByteBuffer2: 1,692,674
Repeat String: 1,528,587
ByteBuffer: 1,064,559
Repeat String2: 263,168
Manual Build: 25,102
Output Stream: 6,553ByteBuffer2: 1,643,425
Repeat String: 1,476,806
ByteBuffer: 1,051,993
Repeat String2: 255,554
Manual Build: 24,505
Output Stream: 6,502
In general, using ByteBuffer with a cached instance of the ByteBuffer Java class is the fastest. But, if you look at the first run, the repeatString() approach - with charsetDecode() - actually won. I'm kind of blown away that the repeatString() approach was so fast! I thought for sure it was going to be a dog on performance.
That said, there are two repeatString() tests - one that uses charsetDecode() and one that uses the underlying Java method, .getBytes(). Oddly enough, the .getBytes() approach was super slow. I'm surprise that there is a measurable difference between these two approaches. I guess the .getBytes() method is doing something unexpected.
In most situations, it probably doesn't matter which technique you use for creating byte arrays / binary values in ColdFusion. But, the ByteBuffer approach (with a cached class) is generally the fastest. Of course, the repeatString() approach is a surprisingly close second in terms of performance; and, it's certainly the easiest one to use if you don't care what values are in the resultant byte array.
Want to use code from this post? Check out the license.
Reader Comments