Why is StringBuilder slower than StringBuffer?

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

Does this have something to do with optimizations made by JIT? Does anyone know why StringBuffer will be faster than StringBuilder, although its methods are synchronized?

Here's the code and test 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)); } } 


Test results:

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


UPDATE:

Thanks to everyone. Problem with heating. When some warm-up code was added, the reference values ​​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 general ratios are consistent with what was expected.

+7
source share
5 answers

I looked at your code, and the most likely reason StringBuilder seems to be slower is because your test does not take into account the effects of JVM warming up. In this case:

  • launching the JVM will result in a significant amount of garbage to be dealt with, and
  • JIT compilation may hit partially, despite the run.

Any or both of these can add to the time measured for the StringBuilder part of your test.

Read the answers to this question for more information: How to write the right micro-test in Java?

+20
source

In both cases, the same code from java.lang.AbstractStringBuilder , and both instances are created with the same capacity (16).

The only difference is using synchronized on the initial call.

I conclude that this is an artifact of measurement.

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 is not overridden)

This blog post says more:

  • micro benchmarking complexity
  • the fact that you do not publish the "results" of the test without looking a bit at what you measured (here is the general superclass)

Note that the β€œslow” synchronization in the recent JDK can be considered a myth. All the tests I have done or read are that, as a rule, there is no reason to lose a lot of time avoiding synchronization.

+5
source

When you run this code yourself, you will see a variable result. Sometimes StringBuffer is faster, and sometimes StringBuilder is faster. The likely reason for this may be the time spent on the JVM warmup before using StringBuffer and StringBuilder , as pointed out by @Stephen, which can change over several runs.

This is the result of 4 runs that I did: -

 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 numbers cannot be predicted on the basis of only the 4th performance.

+2
source

I suggest

  • splitting of each cycle into a separate method, therefore optimization of one does not affect the other.
  • ignore the first 10K iterations
  • run the test for at least 2 seconds.
  • run the test several times to ensure reproducibility.

When you run the code less than 10,000 times, it cannot generate code that will be compiled as the default value -XX:CompileThreshold=10000 . Part of this is collecting statistics on how best to optimize the code. However, when the loop starts compilation, it runs it for the whole method, which can make subsequent loops or look a) better, since they were compiled before they started. B) worse because they are compiled without statistics.


Consider the following code

 public static void main(String... args) { int runs = 1000; for (int i = 0; i < runs; i++) String.valueOf(i); System.out.printf("%-10s%-10s%-10s%-9s%-9s%n", "+ oper", "SBuffer", "SBuilder", "+/Buff", "Buff/Builder"); for (int t = 0; t < 5; t++) { long sConcatTime = timeStringConcat(runs); long sBuffTime = timeStringBuffer(runs); long sBuilderTime = timeStringBuilder(runs); System.out.printf("%,7dns %,7dns %,7dns ", sConcatTime / runs, sBuffTime / runs, sBuilderTime / runs); System.out.printf("%8.2f %8.2f%n", (double) sConcatTime / sBuffTime, (double) sBuffTime / sBuilderTime); } } public static double dontOptimiseAway = 0; private static long timeStringConcat(int runs) { long sConcatStart = System.nanoTime(); for (int j = 0; j < 100; j++) { String s = ""; for (int i = 0; i < runs; i += 100) { s += String.valueOf(i); } dontOptimiseAway = Double.parseDouble(s); } return System.nanoTime() - sConcatStart; } private static long timeStringBuffer(int runs) { long sBuffStart = System.nanoTime(); for (int j = 0; j < 100; j++) { StringBuffer buff = new StringBuffer(); for (int i = 0; i < runs; i += 100) buff.append(i); dontOptimiseAway = Double.parseDouble(buff.toString()); } return System.nanoTime() - sBuffStart; } private static long timeStringBuilder(int runs) { long sBuilderStart = System.nanoTime(); for (int j = 0; j < 100; j++) { StringBuilder buff = new StringBuilder(); for (int i = 0; i < runs; i += 100) buff.append(i); dontOptimiseAway = Double.parseDouble(buff.toString()); } return System.nanoTime() - sBuilderStart; } 

prints with runs = 1000

 + oper SBuffer SBuilder +/Buff Buff/Builder 6,848ns 3,169ns 3,287ns 2.16 0.96 6,039ns 2,937ns 3,311ns 2.06 0.89 6,025ns 3,315ns 2,276ns 1.82 1.46 4,718ns 2,254ns 2,180ns 2.09 1.03 5,183ns 2,319ns 2,186ns 2.23 1.06 

however, if you increase the number of runs = 10,000

 + oper SBuffer SBuilder +/Buff Buff/Builder 3,791ns 400ns 357ns 9.46 1.12 1,426ns 139ns 113ns 10.23 1.23 323ns 141ns 117ns 2.29 1.20 317ns 115ns 78ns 2.76 1.47 317ns 127ns 103ns 2.49 1.23 

and if we increase the runs to 100,000, I get

 + oper SBuffer SBuilder +/Buff Buff/Builder 3,946ns 195ns 128ns 20.23 1.52 2,364ns 113ns 86ns 20.80 1.32 2,189ns 142ns 95ns 15.34 1.49 2,036ns 142ns 96ns 14.31 1.48 2,566ns 114ns 88ns 22.46 1.29 

Note. Operation + slowed down because the time complexity of the cycle is O (N ^ 2)

+2
source

I modified your code a bit and added heating loops. My observations in most cases correspond to the fact that StringBuilder is faster in most cases.

I run in a Ubuntu12.04 box that runs on Windows 7 in practice and has 2 GB of RAM allocated to the virtual machine.

 public class StringOps { public static void main(String args[]) { for(int j=0;j<10;j++){ StringBuffer buff = new StringBuffer(); for(int i=0; i<1000; i++) { buff.append(i); } buff = new StringBuffer(); long sBuffStart = System.nanoTime(); for(int i=0; i<10000; i++) { buff.append(i); } long sBuffEnd = System.nanoTime(); StringBuilder builder = new StringBuilder(); for(int i=0; i<1000; i++) { builder.append(i); } builder = new StringBuilder(); long sBuilderStart = System.nanoTime(); for(int i=0; i<10000; i++) { builder.append(i); } long sBuilderEnd = System.nanoTime(); if((sBuffEnd-sBuffStart)>(sBuilderEnd-sBuilderStart)) { System.out.println("String Builder is faster") ; } else { System.out.println("String Buffer is faster") ; } } } 

}

Results:

 String Builder is faster String Builder is faster String Builder is faster String Builder is faster String Buffer is faster String Builder is faster String Builder is faster String Builder is faster String Builder is faster String Builder is faster 
+1
source

All Articles