String Concatenation Revisited
- Basic Thread Advice
- Dealing with NullPointerExceptions
- Changing the Current Directory
- String Concatenation Options
- String Concatenation Revisited
I had intended to do some follow up numbers to my previous post but I got a bit sidetracked by work and the like. My simple tests all work with one String that's created then thrown away. This test helped me resolve the question I had when I started down that road but stops short of a more general answer. Then I saw this pingback which led me here. There's some nice analysis and insights to consider. So given the shortcomings of my little benchmark and the comments there, I wanted to expand my test a bit and see what things look like when the loop doesn't throw away the data. The test is simple enough again:
import java.util.*; import java.text.*; public class ConcatenationTest { private static long concat(int count) { long start = System.currentTimeMillis(); for(int x = 0; x < count; x++) { String s = "Loop " + x + " of " + count + " iterations."; } return System.currentTimeMillis() - start; } private static long append(int count) { long start = System.currentTimeMillis(); for(int x = 0; x < count; x++) { String s = new StringBuilder("Loop ") .append(x) .append(" of ") .append(count) .append("iterations.") .toString(); } return System.currentTimeMillis() - start; } private static long concatAcrossLoops(int count) { long start = System.currentTimeMillis(); String s = ""; for(int x = 0; x < count; x++) { s += "Loop " + x + " of " + count + " iterations."; } long time = System.currentTimeMillis() - start; System.out.println("concatAcrossLoops time = " + time); return time; } private static long appendAcrossLoops(int count) { long start = System.currentTimeMillis(); StringBuilder s = new StringBuilder(); for(int x = 0; x < count; x++) { s.append("\nLoop ") .append(x) .append(" of ") .append(count) .append("iterations."); } long time = System.currentTimeMillis() - start; System.out.println("appendAcrossLoops time = " + time); return time; } public static void main(String[] args) { int count = 10000; List concats = new ArrayList(); List appends = new ArrayList(); List concatsAcross = new ArrayList(); List appendsAcross = new ArrayList(); for(int x = 0; x < 10; x++) { concats.add(concat(count)); appends.add(append(count)); concatsAcross.add(concatAcrossLoops(count)); appendsAcross.add(appendAcrossLoops(count)); } String header = "concats appends concats across loops appends across loops"; String format = "%7d %9d %22d %22d\n"; System.out.println(header); for(int x = 0; x < 10; x++) { System.out.printf(format, concats.get(x), appends.get(x), concatsAcross.get(x), appendsAcross.get(x)); } } } |
And then the results:
| concats | appends | concats across loops | appends across loops |
|---|---|---|---|
| 48 | 14 | 18990 | 1276 |
| 27 | 11 | 14581 | 1442 |
| 4 | 4 | 13206 | 1253 |
| 3 | 3 | 13478 | 1438 |
| 4 | 4 | 12651 | 1444 |
| 4 | 3 | 12485 | 1403 |
| 4 | 3 | 12608 | 1318 |
| 4 | 3 | 13152 | 1312 |
| 3 | 4 | 12535 | 1390 |
| 4 | 3 | 12444 | 1329 |
Notice after the first two loops the numbers for all runs drops. As the JIT compiler kicks in, we get some optimization but as you can see concatenation across loop iterations is incredibly much more expensive. In this case, StringBuilder is still the clear winner.
update
There was a typo in the original test. I was calling toString() in the appendsAcrossLoop test which was entirely unnecessary. (I forgot to remove that call when adapting from the earlier iteration.) The new results are below. I included them here rather than just replacing the table above as it shows just how expensive that toString() is.
| concats | appends | concats across loops | appends across loops |
|---|---|---|---|
| 42 | 15 | 16562 | 4 |
| 5 | 8 | 12564 | 5 |
| 4 | 3 | 11601 | 2 |
| 4 | 2 | 11141 | 2 |
| 4 | 3 | 11025 | 3 |
| 3 | 3 | 11260 | 3 |
| 3 | 3 | 11062 | 3 |
| 3 | 3 | 11738 | 2 |
| 4 | 2 | 11078 | 2 |
| 4 | 2 | 11130 | 3 |
February 23rd, 2009 - 14:33
Is it realistic that you’d call toString() within the loop of appendAcrossLoops()? I know it would only improve the results of something that’s already the clear winner, but I expected you to call toString() after the loop.
February 23rd, 2009 - 16:02
@Brian Jackson
Whoops! You’re right. That’s a typo from a (too) quick adaptation of the early test. I’ll update and rerun it…
February 25th, 2009 - 04:53
Thanks for your update and benchmarking on the topic. As you mention the JIT kicking in after two loops it would be nice to know if this is the client or server VM and if there are differences between the two.
Another side note: System.nanoTime() should be preferred to System.currentTimeMillis() for measuring short periods of time because it’s using the most precise timing available on the platform.
February 25th, 2009 - 10:06
It’s the default VM. I didn’t do any tweaking (not even memory parameters). You’re right about nanoTime(), though. I always forget that method exists now. I learned the millis time years ago and stopped thinking about it. I’ll have to update that part of my playbook, though. Thanks for the reminder.