Menu

As simple as possible, as complex as necessary

String concatenation in Lucee

8 September 2015

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:

ACF9
&=24ms
ArrayAppend9ms

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:

ACF9
&=722ms
ArrayAppend9ms

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:

Lucee
&=718ms
ArrayAppend17ms
ArrayMember21ms

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 ) );
ACF9
&=22308ms
ArrayAppend72ms
Lucee
&=22658ms
ArrayAppend478ms

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:

ACF9
Java120ms
Lucee
Java38ms

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;
Lucee
savecontent17ms

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.

Posted on . Updated

Comments

  • Formatting comments: See this list of formatting tags you can use in your comments.
  • Want to paste code? Enclose within <pre><code> tags for syntax higlighting and better formatting and if possible use script. If your code includes "self-closing" tags, such as <cfargument>, you must add an explicit closing tag, otherwise it is likely to be mangled by the Disqus parser.
Back to the top