Tag: benchmarks

The String Thing: Java String Concatenation Performance

I’m researching an upcoming performance-related article, and it occurred to me that I had always just accepted it as given that concatenating Strings using the plus operator in a loop is bad.  First I used StringBuffer instead (back in the day), and then StringBuilder because StringBuffer is synchronized. Occasionally, I started using Guava’s Joiner (which has nice, clean syntax, but involves a 3rd-party dependency). Now Java 8 offers a few new approaches to String concatenation, the most eloquent of which is String.join().

So, rather than rest on received wisdom, I thought I’d put these different approaches to the test using JMH benchmarks.  The source code for my benchmarks is here. It concatenates a list of 20000 Strings into one underscore-delimited String in 7 different ways.

Here are the results (on an old windows 7 laptop with 4MB of overly-solicited RAM and 2 cores, so your mileage may vary) – lower scores are faster:

Benchmark                                                 Mode  Cnt       Score       Error  Units
concatenationUsingGuavaJoiner                             avgt   20      70.193 ±     2.456  ns/op
concatenationUsingPlus                                    avgt   20  231599.954 ± 20386.920  ns/op
concatenationUsingStringBuffer                            avgt   20      58.894 ±     3.943  ns/op
concatenationUsingStringBuilder                           avgt   20      52.580 ±     1.514  ns/op
concatenationUsingStringJoin                              avgt   20      69.401 ±     5.827  ns/op
concatenationUsingStringJoinerInStreamWithForEachOrdered  avgt   20      64.491 ±     3.789  ns/op
concatenationUsingStringJoinerOnForLoop                   avgt   20      69.301 ±     6.815  ns/op

Briefly, this shows that concatenation using ‘+’ is truly awful (more than 4300 times slower than the StringBuilder approach!). All other approaches are roughly equivalent in performance, though StringBuilder runs about 30% faster than the syntactically superior approaches. The biggest surprise for me was how little difference there is between StringBuffer and StringBuilder (StringBuilder is about 12% faster). I was also a bit surprised that the compiler/JVM doesn’t do a better job of optimizing the ‘+’-based concatenation, given that this is a known issue since the 1990’s (it turns out the received wisdom of old still applies).

So, if you must have speed at all costs, the winner is:

	@Benchmark
	public String concatenationUsingStringBuilder() {
		// The approved pre-Java-8 approach
		StringBuilder result = new StringBuilder();
		for (String s : sourceList) {
			if (result.length() > 0) {
				result.append(SEPARATOR);
			}
			result.append(s);
		}
		return result.toString();
	}

Otherwise (if having readable, maintainable code is worth more in the long run than a minor performance gain in a few parts of the code), here’s the real winner going forward:

	@Benchmark
	public String concatenationUsingStringJoin() {
		// The simplest, most concise Java-8 approach
		return String.join("" + SEPARATOR, sourceList);
	}

Addendum: Before someone adds a comment about it, let me say that for readability with String join, the code would be prettier if SEPARATOR were a String constant instead of a char constant.  Then you’d just have:

return String.join(SEPARATOR, sourceList);

I didn’t fix it in my benchmark because it’s a reminder that String.join() does not accept a character constant. Fixing it would have no effect on performance, which I can prove with  the output from javap -c on my benchmark class. It shows that the compiler has converted “” + SEPARATOR to the String constant “_”, which is loaded with the ldc bytecode).

Advertisements