The quick and dirty way to concatenate strings in Java is to use the concatenation operator (+). This will yield a reasonable performance if you need to combine two or three strings (fixed-size). But if you want to concatenate n strings in a loop, the performance degrades in multiples of n. Given that String is immutable, for large number of string concatenation operations, using (+) will give us a worst performance. But how bad ? How StringBuffer, StringBuilder or String.concat() performs if we put them on a performance test ?. This article will try to answer those questions.

We will be using Perf4J to calculate the performance, since this library will give us aggregated performance statistics like mean, minimum, maximum, standard deviation over a set time span. In the code, we will concatenate a string (*) repeatedly 50,000 times and this iteration will be performed 21 times so that we can get a good standard deviation. The following methods will be used to concatenate strings.

And finally we will look at the byte code to see how each of these operations perform. Let’s start building the class. Note that each of the block in the code should be wrapped around the Perf4J library to calculate the performance in each iteration. Let’s define the outer and inner iterations first.

private static final int OUTER_ITERATION=20;
private static final int INNER_ITERATION=50000;

Now let’s implement each of the four methods mentioned in the article. Nothing fancy here, plain implementations of (+), String.concat(), StringBuffer.append() & StringBuilder.append().

String addTestStr = "";
String concatTestStr = "";
StringBuffer concatTestSb = null;
StringBuilder concatTestSbu = null;

for (int outerIndex=0;outerIndex<=OUTER_ITERATION;outerIndex++) {
    StopWatch stopWatch = new LoggingStopWatch("StringAddConcat");
    addTestStr = "";
    for (int innerIndex=0;innerIndex<=INNER_ITERATION;innerIndex++)
 addTestStr += "*";
    stopWatch.stop();
}        

for (int outerIndex=0;outerIndex<=OUTER_ITERATION;outerIndex++) {
    StopWatch stopWatch = new LoggingStopWatch("StringConcat");
    concatTestStr = "";
    for (int innerIndex=0;innerIndex<=INNER_ITERATION;innerIndex++)
 concatTestStr = concatTestStr.concat("*");
    stopWatch.stop();
}

for (int outerIndex=0;outerIndex<=OUTER_ITERATION;outerIndex++) {
    StopWatch stopWatch = new LoggingStopWatch("StringBufferConcat");
    concatTestSb = new StringBuffer();
    for (int innerIndex=0;innerIndex<=INNER_ITERATION;innerIndex++)
 concatTestSb.append("*");
    stopWatch.stop();
}

for (int outerIndex=0;outerIndex<=OUTER_ITERATION;outerIndex++) {
    StopWatch stopWatch = new LoggingStopWatch("StringBuilderConcat");
    concatTestSbu = new StringBuilder();
    for (int innerIndex=0;innerIndex<=INNER_ITERATION;innerIndex++)
 concatTestSbu.append("*");
    stopWatch.stop();
}

Let’s run this program and generate the performance metrics. I ran this program in a 64-bit OS (Windows 7), 32-bit JVM (7-ea), Core 2 Quad CPU (2.00 GHz) with 4 GB RAM.

The output from the 21 iterations of the program is plotted below.

 String_Perf_Chart_2

Well, the results are pretty conclusive and as expected. One interesting point to notice is how better String.concat performs. We all know String is immutable, then how the performance of concat is better. To answer the question we should look at the byte code. I have included the whole byte code in the download package, but let’s have a look at the below snippet.

45: new #7; //class java/lang/StringBuilder
48: dup
49: invokespecial #8; //Method java/lang/StringBuilder."<init>":()V
52: aload_1
53: invokevirtual #9; //Method java/lang/StringBuilder.append:
    (Ljava/lang/String;)Ljava/lang/StringBuilder;
56: ldc #10; //String *
58: invokevirtual #9; //Method java/lang/StringBuilder.append:
    (Ljava/lang/String;)Ljava/lang/StringBuilder;
61: invokevirtual #11; //Method java/lang/StringBuilder.toString:()
    Ljava/lang/String;
64: astore_1

This is the byte code for String.concat(), and its clear from this that the String.concat is using StringBuilder for concatenation and the performance should be as good as String Builder. But given that the source object being used is String, we do have some performance loss in String.concat.

So for the simple operations we should use String.concat compared to (+), if we don’t want to create a new instance of StringBuffer/Builder. But for huge operations, we shouldn’t be using the concat operator, as seen in the performance results it will bring down the application to its knees and spike up the CPU utilization. To have the best performance, the clear choice is StringBuilder as long as you do not need thread-safety or synchronization.

The full source code, compiled class & the byte code is available for download in the below link.

Download Source, Class & Byte Code: String_Concatenation _Performance.zip



Published by Venish Joe on Sunday, November 08, 2009

18 Comments:

  1. Anonymous said...
    How did you get the byte code for the class ? Should I download some tools ?
    Venish Joe said...
    @Anonymous - You can get the bytecode for any class using javap which ships along with JDK. You can find more information in the below link.

    http://java.sun.com/j2se/1.4.2/docs/tooldocs/windows/javap.html
    Michael said...
    Surely for the concat() test to be functionally equivalent to the others you would have to replace the line

    concatTestStr.concat("*");

    by

    concatTestStr = concatTestStr.concat("*");

    How does that affect the result?
    Venish Joe said...
    @Michael - That's an interesting question. I modified the code as you mentioned and the performance did degrade for String.concat(), but not as worse as the (+) operator.

    It took an average of 1650.9 ms compared to the 3.3 ms it took before.

    I think the loss in performance id due to the over head of assigning the value back to the same variable again and again in the loop.
    Dror Harari said...
    Both the x += y and the x = x.concat(y) assign the value to the same variable, you should look for the performance difference elsewhere. Look at the generated code...
    Venish Joe said...
    @Michael, @Dror Harari - Thanks for pointing, I updated the post with the new numbers. The old code was not assinging the updated string to the object that's the reason for the better performance.
    Anonymous said...
    You say that the results are conclusive and expected. I would say that the difference between "+" and StringBuilder is unexpected and consequently the results are far from conclusive. The std dev. on all runs are way too high which suggest that there are interferences in your benchmarks. Also there is no warmup in the code.

    One thing to note, StringBuilder and StringBuffer all delegate their calls to a common abstract super class. The only difference is synchronization. Synchronization behaves very differently on single core and multi-core and is also very sensitive to the version of the VM you are using as well as any command line settings. IOWs, it is very easy to make StringBuffer worse than any of the numbers you've published here.
    Anonymous said...
    Please change the positions of the + and concat tests. What do you get?

    Using concat instead of + isn't a good idea, as the java compiler will optimize the + statements where possible.

    String myVar = "abcdef";
    String myVar = "a"+"b"+"c"+"d"+"e"+"f";
    Those statement will be exactly the same at bytecode level.
    Venish Joe said...
    @Anonymous

    I ran the tests more than 5 times with default run-time parameters and the difference in the standard deviation was almost negligible. I am not sure if running in a EA JRE 7 made any difference, probably I will try this in JRE6 in a 32bit machine and see if it makes any difference.

    @Anonymous

    Switching the positions didn't make any difference, I don't see why that would make any difference. Java compiler will optimize the (+) statements as you mentioned as long as they are in the same statement, but in a loop there wouldn't be any big optimization by compiler.
    Rob said...
    There have been so very many articles and blog posts about the "pitfalls" of string concatenation in Java, and I'm still waiting for the one that starts with "there was a performance hotspot in our code that we started researching...".

    It's all entirely academic unless it solves an *actual* performance problem in real-live code, and it results in a lot of bad code by new developers who waste their time rewriting working code to "clean up all that unoptimal string concatenation"... accomplishing nothing but making code less readable and introducing bugs.

    What IS an example of commonly written code that involves concatenating large numbers of Strings in a loop? I can't think of any time that situation turned up in my 12 years of Java development.

    What I *have* seen is lots of ugly code where people are jumping through hoops to avoid using "+" when, for example, generating one line from 3 fields "city, state zip" -- an utterly useless optimization -- so that after a while I kinda get pissed off seeing this optimization pop up again in another post/article/etc.. Any discussion of an optimization needs to cover when to use it (with real-life examples...), because there are always times they should not be used, often for reasons of readability/maintainability.
    williamlouth said...
    "What IS an example of commonly written code that involves concatenating large numbers of Strings in a loop? I can't think of any time that situation turned up in my 12 years of Java development."

    I am actually writing a blog post today about this very issue and you be very surprised how common it is once you filter out a lot of noise in the execution of a web request. Does one need to time it? Absolutely NOT. In fact the customer case that serves as the basis for this blog entry used StringBuilder for the most part. The problem with String(Buffers/Builders) is that they are used wrongly as generic output queues. I will post the link later when the article is finished.

    Is there a point to the above comparisons. NO.
    Anonymous said...
    Switching positions can make a difference, because the JIT-compiler needs some warmup. When JIT kicks in with runtime optimizations, the numbers change dramatically.
    williamlouth said...
    At the end of the day this whole benchmark is pointless as no one code in an real-world executes like any of the scenarios tested above. The most common case is someone creating a string (or buffer or builder) and performing a few appends or inserts. This is what should be tested if one really has too much "waste" time with oneself. But then you would be much better off benchmarking the creation and copying of char[] arrays which is what is effectively happening underneath the thin service veneer of these classes.

    What is much more useful to note in an application is how many times one allocs/inserts/appends/resizes/tostrings builders and buffers in the course of an execution which is the point of my upcoming blog entry.
    Anonymous said...
    For the sake of just looking good. Compare the test above with just a print("*"); and at the end print("\n"); and see if it's just plain faster. I mean the code above looks cool, however, is it faster.
    I mean the object above can do a lot more, but, I just want a fast printout. And any memory leaks?
    Anonymous said...
    You should always specify either -client or -server as parameter or at least check with -version what will be the default for your setup. Otherwise you'll never know what compiler optimizations will be used. Basically all Java benchmarks are useless without knowing what -client/-server flag was used. With -server depending on the test one can basically see some code to be "removed" or it can make the difference between and endless loop and ending loop (try threading with volatile)...
    Anonymous said...
    I don't think this is a fair analysis of the + operator. Performing this inside a loop will obviously result in allocating a new String on every iteration. As some of the previous commentors indicated, this is not seen often in practice. What I do most often is generate log reports using a variety of known data as follows:

    String rep = "Name: " + A.getName() +
    + "\nDescription: " + A.getDesc() +
    + "\nSize: " + A.getSize() +
    + "\nStatus: " + A.generateStatus();

    Thus, I basically have the case of;

    String rep = A + B + C + D + E + F + G + H;

    Now, there are a number of ways that the compiler can behave. The way I've always thought it behaved was as follows:
    Allocate String for A+B
    Allocate String for (A+B)+C
    Allocate String for (A+B+C)+D
    ...
    Allocate String for (A+B+C+D+E+F+G)+H

    Thus, there are 7 Strings allocated for the purposes of concatenating 8 strings.

    There are a few obvious optimizations that the compiler could make:

    Automatically concatenate adjacent constant Strings. This would be known at compile time. For example,

    String res = "A" + "B" + C.toString();

    could be optimized by the compiler to

    String res = "AB" + C.toString();

    I would be surprised if the compiler doesn't already do this.

    Another optimization it could do is get the total length of all the Strings in the concat list and allocate a single String to hold them all. I don't know if the compiler can do this, but if it did, then I would say that for most uses, the + operator is optimal.
    Anonymous said...
    Thanks for this post. I wasn't aware the difference between (+), concat() and using StringBuffer/StringBuilder was so big. I have seen a dramatic improvement in performance in my app (outputing HTML). I thought it was I/O related, but it had everything to do with naively concatening Strings with (+) in loops.
    Dharshna Gamage said...
    I also have done this test for small size of loop and got the time in nano seconds.

    Then Out put was as below.

    String Builder Concat Time :8938
    String Concat Time :13574
    String Add Concat(+) Time :83429
    String Buffer Concat Time :266840

Post a Comment



Post a Comment