Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why is StringBuilder slower than StringBuffer?

In this example, StringBuffer is actually faster than StringBuilder, whereas I would have expected opposite results.

Is this something to do with optimizations being made by the JIT ? Does anyone know why StringBuffer would be faster than StringBuilder, even though it's methods are synchronized ?

Here's the code and the benchmark results:

public class StringOps {

    public static void main(String args[]) {

        long sConcatStart = System.nanoTime();
        String s = "";
        for(int i=0; i<1000; i++) {
            s += String.valueOf(i);
        }
        long sConcatEnd = System.nanoTime();

        long sBuffStart = System.nanoTime();
        StringBuffer buff = new StringBuffer();
        for(int i=0; i<1000; i++) {
            buff.append(i);
        }
        long sBuffEnd = System.nanoTime();

        long sBuilderStart = System.nanoTime();
        StringBuilder builder = new StringBuilder();
        for(int i=0; i<1000; i++) {
            builder.append(i);
        }
        long sBuilderEnd = System.nanoTime();

        System.out.println("Using + operator : " + (sConcatEnd-sConcatStart) + "ns");
        System.out.println("Using StringBuffer : " + (sBuffEnd-sBuffStart) + "ns");
        System.out.println("Using StringBuilder : " + (sBuilderEnd-sBuilderStart) + "ns");

        System.out.println("Diff '+'/Buff = " + (double)(sConcatEnd-sConcatStart)/(sBuffEnd-sBuffStart));
        System.out.println("Diff Buff/Builder = " + (double)(sBuffEnd-sBuffStart)/(sBuilderEnd-sBuilderStart));
    }
}


Benchmark results:

Using + operator : 17199609ns
Using StringBuffer : 244054ns
Using StringBuilder : 4351242ns
Diff '+'/Buff = 70.47460398108615
Diff Buff/Builder = 0.056088353624091696


UPDATE:

Thanks to everyone. Warmup was indeed the problem. Once some warmup code was added, the benchmarks changed to:

Using + operator : 8782460ns
Using StringBuffer : 343375ns
Using StringBuilder : 211171ns
Diff '+'/Buff = 25.576876592646524
Diff Buff/Builder = 1.6260518726529685


YMMV, but at least the overall ratios agree with what would be expected.

like image 671
Parag Avatar asked Oct 26 '12 07:10

Parag


3 Answers

I had a look at your code, and the most likely reason that StringBuilder it appears to be slower is that your benchmark is not properly taking account of the effects of JVM warmup. In this case:

  • the JVM startup will generate an appreciable amount of garbage which needs to be dealt with, and
  • JIT compilation may kick in partway though the run.

Either or both of these could add to the time measured for the StringBuilder part of your test.

Please read the answers to this Question for more details: How do I write a correct micro-benchmark in Java?

like image 197
Stephen C Avatar answered Sep 28 '22 16:09

Stephen C


The exact same code, from java.lang.AbstractStringBuilder, is used in both cases, and both instances are created with the same capacity (16).

The only difference is the use of synchronized at the initial call.

I conclude this is a measurement artifact.

StringBuilder :

228    public StringBuilder append(int i) {
229        super.append(i);
230        return this;
231    }

StringBuffer :

345    public synchronized StringBuffer append(int i) {
346        super.append(i);
347        return this;
348    }

AbstractStringBuilder :

605     public AbstractStringBuilder append(int i) {
606         if (i == Integer.MIN_VALUE) {
607             append("-2147483648");
608             return this;
609         }
610         int appendedLength = (i < 0) ? Integer.stringSize(-i) + 1
611                                      : Integer.stringSize(i);
612         int spaceNeeded = count + appendedLength;
613         if (spaceNeeded > value.length)
614             expandCapacity(spaceNeeded);
615         Integer.getChars(i, spaceNeeded, value);
616         count = spaceNeeded;
617         return this;
618     }


110     void expandCapacity(int minimumCapacity) {
111         int newCapacity = (value.length + 1) * 2;
112         if (newCapacity < 0) {
113             newCapacity = Integer.MAX_VALUE;
114         } else if (minimumCapacity > newCapacity) {
115             newCapacity = minimumCapacity;
116         }
117         value = Arrays.copyOf(value, newCapacity);
118     }

(expandCapacity isn't overrided)

This blog post says more about :

  • the difficulty there is in micro-benchmarking
  • the fact that you shoudln't post "results" of a benchmark without looking a little at what you measured (here the common superclass)

Note that the "slowness" of synchronized in recent JDK can be considered a myth. All tests I made or read conclude there is generally no reason to lose much time avoiding the synchronizations.

like image 20
Denys Séguret Avatar answered Sep 28 '22 16:09

Denys Séguret


When you run that code on yourself, you would see a varying result. Sometimes StringBuffer is faster and sometimes StringBuilder is faster. The likely reason for this may be the time taken for JVM warmup before using StringBuffer and StringBuilder as stated by @Stephen, which can vary on multiple runs.

This is the result of 4 runs I made: -

Using StringBuffer : 398445ns
Using StringBuilder : 272800ns

Using StringBuffer : 411155ns
Using StringBuilder : 281600ns

Using StringBuffer : 386711ns
Using StringBuilder : 662933ns

Using StringBuffer : 413600ns
Using StringBuilder : 270356ns

Of course the exact figures cannot be predicted based on just 4 execution.

like image 41
Rohit Jain Avatar answered Sep 28 '22 15:09

Rohit Jain