String concatenation in Lucee
In my last post I noted the significant average request speed gains we've seen having moved from ColdFusion 9 to Lucee. However, I was surpised to find that in one particular area Lucee seems to be slower.
In recent years, I've got into the habit of using arrays to build strings iteratively, converting them back to a string at the end of the process. Why do this intead of simply concatenating using the &=
operator? In short: speed.
Test function
Here's a function that puts each method through its paces:
struct function concatenationSpeed( required string stringToAdd,required string method,required numeric iterations ){
var start = GetTickCount();
var finalString="";
switch( method ){
case "&=":
for( var i=1; i <= iterations; i++ ){
finalString &= stringToAdd;
}
break;
case "ArrayAppend":
var tempResult=[];
for( var i=1; i <= iterations; i++ ){
ArrayAppend( tempResult,stringToAdd );
}
finalString=ArrayToList( tempResult,"" );
break;
}
var end = GetTickCount();
var result={};
result[ method ]=end-start & "ms";
return result;
}
Let's see how each method fares at building a string made of ten thousand single letters.
WriteDump( concatenationSpeed( stringToAdd="a",method="&=",iterations=10000 ) );
WriteDump( concatenationSpeed( stringToAdd="a",method="ArrayAppend",iterations=10000 ) );
Under ACF9 the times on my dev machine are:
&= | 24ms |
---|---|
ArrayAppend | 9ms |
Using arrays is almost three times faster, but the absolute difference is only 15ms in this case. Worth worrying about? Let's repeat with a longer string.
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="&=",iterations=10000 ) );
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="ArrayAppend",iterations=10000 ) );
This yields:
&= | 722ms |
---|---|
ArrayAppend | 9ms |
Eighty times faster. Normally I'd go for the simpler syntax, but it's not uncommon for concatenation operations to run into the hundreds or thousands, and this is a significant peformance difference which could impact server resources.
Member functions
Moving from CF9 to Lucee, the ability to use member functions has made the array syntax less jarring. In fact, .append()
to me seems more expressive of concatenation than &=
. Let's add the following variation to our test function's switch.
case "ArrayMember":
var tempResult=[];
for( var i=1; i <= iterations; i++ ){
tempResult.Append( stringToAdd );
}
finalString=tempResult.ToList( "" );
break;
Now we'll run some tests on Lucee (4.5.1.023).
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="&=",iterations=10000 ) );
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="ArrayAppend",iterations=10000 ) );
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="ArrayMember",iterations=10000 ) );
This yields:
&= | 718ms |
---|---|
ArrayAppend | 17ms |
ArrayMember | 21ms |
Again the absolute numbers are tiny in this case, but nonethless ACF9 seems to be about twice as fast using arrays (note: I ran all the tests several times in succession to eliminate caching effects). Not really what I was expecting.
More interations
Let's crank the iterations up from ten to fifty thousand and compare the two engines.
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="&=",iterations=50000 ) );
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="ArrayAppend",iterations=50000 ) );
&= | 22308ms |
---|---|
ArrayAppend | 72ms |
&= | 22658ms |
---|---|
ArrayAppend | 478ms |
At this number of loops, the poor performance of &=
becomes very clear, but it's the fact that using arrays is seven times slower in Lucee than in ACF9 which is most striking.
Using Java StringBuilder
For a more perfomant concatenation method in Lucee, let's drop down to Java and add this final case to our test function.
case "Java":
var tempResult=CreateObject( "Java","java.lang.StringBuilder" ).init();
for( var i=1; i <= iterations; i++ ){
tempResult.append( stringToAdd );
}
finalString=tempResult.ToString();
break;
Running this method...
WriteDump( concatenationSpeed( stringToAdd="the quick brown fox jumped over the lazy dog",method="Java",iterations=50000 ) );
...produces the following results:
Java | 120ms |
---|
Java | 38ms |
---|
Blazing fast and much more in line with expectations. Although more complex because of the Java invocation, there are no additional lines of code, and the appending line within the loop doesn't need to change from the array member function version.
Conclusion
I'm very much of the view that improvements to systems are more likely to result from clearer code than performance optimisations at the expense of syntactic clarity. But where tangible resource efficiencies can be made without too much of an increase in complexity, they're worth considering.
Update: using savecontent
Brad Wood suggests an alternative to using Java in Lucee which seems to be just as quick (actually quicker).
case "savecontent":
savecontent variable="finalString"{
for( var i=1; i <= iterations; i++ ){
WriteOutput( stringToAdd );
}
}
break;
savecontent | 17ms |
---|
As well as avoiding the Java call and therefore staying within the bounds of documented CFML, it's also slightly less code. Thanks Brad!
Update: run your own tests
As I explained to Brad in the comments, I'm only interested here in comparing Lucee with our previous CFML engine, ColdFusion 9. If you want to see how later versions of Adobe's product (or indeed newer releases of Lucee) fare, you are welcome to download the code from GitHub and run it using your own environment.
Comments