Reputation: 12453
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.
Upvotes: 9
Views: 4123
Reputation: 533492
I suggest
When you run code less than 10000 times it might not trigger the code to be compiled as the default -XX:CompileThreshold=10000
. Part of the reason it does this is to collect statistics on how to best optimise the code. However, when a loop triggers compilation, it triggers it for the whole method which can make later loops either look a) better as they are compiled before they start b) worse as the are compiled without collecting any 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: The +
operation has slowed down as the time complexity of the loop is O(N^2)
Upvotes: 2
Reputation: 535
I have modified your code a little bit and added the warmup loops. My observations are consistent most of the time that StringBuilder is faster most of the times.
I am running on Ubuntu12.04 box which runs on windows 7 virtually and have 2 GB RAM allocated to the VM.
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 are :
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
Upvotes: 1
Reputation: 382112
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 :
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.
Upvotes: 5
Reputation: 718758
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:
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?
Upvotes: 23
Reputation: 213223
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.
Upvotes: 2