StringBuffer vs. StringBuilder performance comparison

Even if the StringBuilder was around for a while now, many people still use StringBuffer in single threaded applications. The main difference is stated in the StringBuffer comments:
“As of release JDK 5, this class has been supplemented with an equivalent class designed for use by a single thread, {StringBuilder}. The StringBuilder class should generally be used in preference to this one, as it supports all of the same operations but it is faster, as it performs no synchronization.”

So StringBuilder is supposed to be faster. But how much faster? Let’s see…

I create a small program to stress test StringBuffer vs. StringBuilder. As a reference I also do straight String concatenation. In a big loop I just add one character to a String/StringBuffer/StringBuilder. I also play with the initial memory allocation for the StringBuilder and StringBuffer cases. Here is the code:

package com.littletutorials.tips;

public class ConcatPerf {
    private static final int ITERATIONS = 100000;
    private static final int BUFFSIZE = 16;

    private void concatStrAdd() {
        System.out.print("concatStrAdd   -> ");
        long startTime = System.currentTimeMillis();
        String concat = "";
        for (int i = 0; i < ITERATIONS; i++) {
            concat += i % 10;
        }
        long endTime = System.currentTimeMillis();
        System.out.print("length: " + concat.length());
        System.out.println(" time: " + (endTime - startTime));
    }

    private void concatStrBuff() {
        System.out.print("concatStrBuff  -> ");
        long startTime = System.currentTimeMillis();
        StringBuffer concat = new StringBuffer(BUFFSIZE);
        for (int i = 0; i < ITERATIONS; i++) {
            concat.append(i % 10);
        }
        long endTime = System.currentTimeMillis();
        System.out.print("length: " + concat.length());
        System.out.println(" time: " + (endTime - startTime));
    }

    private void concatStrBuild() {
        System.out.print("concatStrBuild -> ");
        long startTime = System.currentTimeMillis();
        StringBuilder concat = new StringBuilder(BUFFSIZE);
        for (int i = 0; i < ITERATIONS; i++) {
            concat.append(i % 10);
        }
        long endTime = System.currentTimeMillis();
        System.out.print("length: " + concat.length());
        System.out.println(" time: " + (endTime - startTime));
    }

    public static void main(String[] args) {
        ConcatPerf st = new ConcatPerf();
        System.out.println("Iterations: " + ITERATIONS);
        System.out.println("Buffer    : " + BUFFSIZE);

        st.concatStrBuff();
        st.concatStrBuild();
        st.concatStrAdd();
    }
}

This example allows me to play with the number of iterations, the initial buffer size (not for String concatenation) and which tests I run. Nothing smart, just commenting what I don’t want to run.

The numbers are not really important but the percentage difference is.

The first run with all three tests and the default capacity:


Iterations: 100000
Capacity : 16
concatStrBuff -> length: 100000 time: 16
concatStrBuild -> length: 100000 time: 15
concatStrAdd -> length: 100000 time: 10437

This makes it clear I should never use plain String concatenation in big loops. Of course if you just concatenate a few strings once in a while this doesn’t matter. The explanation is clear from the bytecode generated for the loop in the concatStrAdd() method (fragment):

   L3
    LINENUMBER 15 L3
    ICONST_0
    ISTORE 4
   L4
    GOTO L5
   L6
    LINENUMBER 17 L6
   FRAME APPEND [J java/lang/String I]
    NEW java/lang/StringBuilder
    DUP
    ALOAD 3
    INVOKESTATIC java/lang/String.valueOf(Ljava/lang/Object;)Ljava/lang/String;
    INVOKESPECIAL java/lang/StringBuilder.<init>(Ljava/lang/String;)V
    ILOAD 4
    BIPUSH 10
    IREM
    INVOKEVIRTUAL java/lang/StringBuilder.append(I)Ljava/lang/StringBuilder;
    INVOKEVIRTUAL java/lang/StringBuilder.toString()Ljava/lang/String;
    ASTORE 3
   L7
    LINENUMBER 15 L7
    IINC 4 1
   L5
   FRAME SAME
    ILOAD 4
    LDC 100000000
    IF_ICMPLT L6
   L8

The compiler uses a StringBuilder to concatenate Strings but unfortunately it creates one instance of it at every iteration. Compare it with the code generated for the loop in the concatStrBuild() method (fragment):

   L3
    LINENUMBER 43 L3
    ICONST_0
    ISTORE 4
   L4
    GOTO L5
   L6
    LINENUMBER 45 L6
   FRAME APPEND [J java/lang/StringBuilder I]
    ALOAD 3
    ILOAD 4
    BIPUSH 10
    IREM
    INVOKEVIRTUAL java/lang/StringBuilder.append(I)Ljava/lang/StringBuilder;
    POP
   L7
    LINENUMBER 43 L7
    IINC 4 1
   L5
   FRAME SAME
    ILOAD 4
    LDC 100000000
    IF_ICMPLT L6
   L8

Increasing the initial capacity for StringBuffer and StringBuilder doesn’t make much difference. Clearly with only 100,000 iterations the numbers for StringBuffer and StringBuilder are just noise.


Iterations: 100000
Capacity : 100000
concatStrBuff -> length: 100000 time: 15
concatStrBuild -> length: 100000 time: 16
concatStrAdd -> length: 100000 time: 10594

Let’s crank it up… but without the String concatenation test because it will never finish.


Iterations: 100000000
Capacity : 16
concatStrBuff -> length: 100000000 time: 15142
concatStrBuild -> length: 100000000 time: 10891

Now it is pretty clear StringBuilder is much faster because it avoids synchronization.


Iterations: 100000000
Capacity : 100000000
concatStrBuff -> length: 100000000 time: 14220
concatStrBuild -> length: 100000000 time: 10611

Allocating all the memory at once does make some difference but nothing really spectacular. But if we look at how the memory allocation peaks we get some more food for thought:

Memory

Bump 1: StringBuffer with buffer 100000000
Bump 2: StringBuffer with buffer 16
Bump 3: StringBuilder with buffer 100000000
Bump 4: StringBuilder with buffer 16

So StringBuilder is faster by a good percentage (34% on my machine in this case) but remember that it is not thread safe.

15 thoughts on “StringBuffer vs. StringBuilder performance comparison”

  1. Adam, you are the worst kind of *** on the internet. You understood exactly what he was saying but had to point out a simple typo.

    Next time stick to *** ***.

  2. @Dan

    I used Java 6. I should have pointed this out in the post.

    @Adam V.

    Thanks, I fixed it. Another proof copy/paste is not good for code reuse :)

    @ Pete M.

    Sorry I had to edit your comment. I don’t want to misdirect people searching for hotter topics :)

  3. I haven’t used Java for a couple of years, but shouldn’t you do a “toString” to actually get the final string? I don’t know if it will actually make any difference, though.

  4. You could even argue that StringBuffer is just plain deprecated. Has anyone ever come across a situation where there was a compelling use case for StringBuffer? Is there ever a genuine need to build up a String across multiple threads? Not saying it’s out of the realm of possibility, It just seems a bit of a weird thing to do.

    (Well, to be totally honest, I’ve come across one use for StringBuffer: Matcher.appendReplacement is specified for StringBuffer, not StringBuilder).

  5. I tested the code on my Ubuntu Linux box. The results on OpenJDK 1.6.0-b09 were:

    Iterations: 100000
    Buffer : 16
    concatStrBuff -> length: 100000 time: 109
    concatStrBuild -> length: 100000 time: 26

    Iterations: 100000
    Buffer : 100000
    concatStrBuff -> length: 100000 time: 101
    concatStrBuild -> length: 100000 time: 26

    Iterations: 100000000
    Buffer : 100000000
    concatStrBuff -> length: 100000000 time: 5183
    concatStrBuild -> length: 100000000 time: 1727

    Iterations: 100000000
    Buffer : 16
    concatStrBuff -> length: 100000000 time: 6217
    concatStrBuild -> length: 100000000 time: 2330

    It seems that the speed difference was even more significant on my box. The results for a Sun JVM (1.6.0_06-b02) were almost the same.

  6. You are absolutely correct when saying that people should avoid using synchronized classes where possible. However, (not just) starting with Java 6, sun has invested a lot of effort to reduce synchronization where possible… there’s a quite interesting article regarding this topic over at infoq: http://www.infoq.com/articles/java-threading-optimizations-p1 . Particularly, you might want to check out the -XX+AggressiveOpts flags for your benchmarking.

  7. Very interesting! I have tried to persuade people to use StringBuilder for years. These numbers might help me :-)

    One thing: When I first read your numbers:
    Iterations: 100000
    Capacity : 16
    concatStrBuff -> length: 100000 time: 16
    concatStrBuild -> length: 100000 time: 15
    concatStrAdd -> length: 100000 time: 10437
    I thought the two first numbers were very close, but then I realized that you are probably using the same kind of machine as me. I have made a habit of adding this code to this kind of measurements:
    long starttime = System.currentTimeMillis();
    long endtime = 0;
    long no = 0;
    while ((endtime = System.currentTimeMillis()) == starttime) {no++; }
    MinimumTimeDifference = endtime – starttime;
    System.out.println(“\n\n================================================================================================”);
    System.out.println(“DISCLAIMER: Smallest difference in System time is “+MinimumTimeDifference+” ms, ” +
    “so any time lower than “+(2*MinimumTimeDifference)+” ms is insignificant”);
    System.out.println(“================================================================================================\n\n”);

    This usually displays 15 or 16 milliseconds. This means that any time differences below 16 milliseconds are insignificant.

  8. It really help me a lot …. Thanks :)

    Iterations: 100000
    Buffer : 16
    concatStrBuff -> StrBuff: 147454
    length: 100000 time: 15
    concatStrBuild -> StrBuild: 147454
    length: 100000 time: 0
    concatStrAdd -> length: 100000 time: 8578

  9. This is really interesting and I am all for broader StringBuilder usage, I am used it myself for several times, but I have to point out that you should think twice before using it just because it is not thread safe;-) 4 second time save on hundred million iterations is impressive, but most of common applications usually do not contain such monstrous loops.

  10. Lasse don’t do timing in java like that. It don’t work. Your jvm process may be swapped out of the CPU and then you don’t know how much of that time was spent on your code or waiting in the OS queues…

  11. System.nanoTime() is more accurate than System.currentTimeMillis() and should always be used for benchmarking (in Java 5+).

  12. One thing about your methodology that is somewhat incorrect. When you did:

    for (int i = 0; i < ITERATIONS; i++) {
    concat += i % 10;
    }

    This isn’t purely an equal test; I say this because if you decompile a .class file that has code such as:

    String a = “foo”;
    String b = “bar;

    String c = a + b;

    javac actually writes:

    String c = new StringBuffer( a ).append(b).toString();

    This is different than using +=

    Related discussion here:

  13. Using single thread, you can’t find out the real performance… try in multi threading situation…. sure there you will find out real difference…. b/c StringBuffer is thread safe.

Comments are closed.