String Buffer Example Take Three

[From StringBuffer]

This really blows my mind. Everywhere, everywhere, everywhere, I see Java literature advising me to use StringBuffers everywhere because they're "much faster". Just this week, I saw it in a "performance tips" column. TogetherSoft IDE has a code audit facility that flags string concatenations as a potential performance problem. I actually performed the compilation / disassembly myself because I couldn't believe that so many Java experts could be so wrong. What do you know. They're wrong.

Moral of the story: Java experts are all sheep. The emperor has no clothes.


The other tests here (by LukeGorrie and WayneConrad on StringBufferExample and StringBuffer respectively) have decompiled Java classes. I just wrote a timer. But I did compare doing concatenation in loops and in methods. My test seems to indicate that the StringBuffer method is always faster. I did this test using JDK1.4 on Win2000. Am I missing something?

 public class StringAppendTest {
private int numberOfRepetitions;

static final String string = "One";

public StringAppendTest(int numberOfRepetitions) { this.numberOfRepetitions = numberOfRepetitions; }

public void timingInLoops() { StopWatch plusAppendingWatch = new StopWatch(); StopWatch bufferAppendingWatch = new StopWatch();

plusAppendingWatch.start();

String plusAppended = new String("");

for (int i=0; i<numberOfRepetitions; i++) { plusAppended += string; }

plusAppendingWatch.stop();

bufferAppendingWatch.start();

StringBuffer buffer = new StringBuffer("");

for (int j=0; j<numberOfRepetitions; j++) { buffer.append( string ); }

bufferAppendingWatch.stop();

outputResults("Loops", plusAppendingWatch, bufferAppendingWatch); }

private void outputResults(String message, StopWatch plusAppendingWatch, StopWatch bufferAppendingWatch) { System.out.println( message ); System.out.println("Plus appending took: " + plusAppendingWatch.getTimeElapsed()); System.out.println("Buffer appending took: " + bufferAppendingWatch.getTimeElapsed()); }

public void timingInCalls() { StopWatch plusAppendingWatch = new StopWatch(); StopWatch bufferAppendingWatch = new StopWatch();

plusAppendingWatch.start();

for (int i=0; i<numberOfRepetitions/4; i++) { evaluatePlusAppend(); }

plusAppendingWatch.stop();

bufferAppendingWatch.start();

for (int j=0; j<numberOfRepetitions/4; j++) { evaluateBufferAppend(); }

bufferAppendingWatch.stop();

outputResults("Calls:", plusAppendingWatch, bufferAppendingWatch); }

private void evaluatePlusAppend() { String test = new String("");

test = test + "one" + "one" + "one" + "one"; }

private void evaluateBufferAppend() { StringBuffer buffer = new StringBuffer("");

buffer.append("one").append("one").append("one").append("one"); }

public void run() { timingInLoops(); timingInCalls(); }

public static void main(String args[]) { StringAppendTest test;

if (args[0] != null) { test = new StringAppendTest( Integer.parseInt(args[0]) ); } else { test = new StringAppendTest( 1000 ); }

test.run(); }

class StopWatch { long start = (-1); long end = (-1);

public void start() { start = System.currentTimeMillis(); }

public void stop() { end = System.currentTimeMillis(); }

public long getTimeElapsed() { if (end == (-1) || start == (-1)) { throw new RuntimeException("Invalid state!"); }

return end - start; } } }
Running this produces:

 Loops
 Plus appending took: 45946
 Buffer appending took: 30
 Calls:
 Plus appending took: 31
 Buffer appending took: 20
-- IainLowe

Couple of thoughts: in the loops example, I think it's too tricky for the compiler to optimize, and that it's a case where StringBuffer is the right thing. In the calls example, I think to be fair you have to toString() the StringBuffer at the end, otherwise it's not computing the same function (one results in a String, the other in a StringBuffer). Probably my comments on the other page were misleading, I only meant that StringBuffer wasn't better for the particular examples given, not that it's never good. -- LukeGorrie

Also, in your call tests, you're not actually returning the result. So if the compiler is halfway smart about it, it could optimize the whole thing away into a no-op.


See, where people have been silly is this: The compiler cannot easily optimize the + operator over multiple loop iterations. Because of this, it has to make a new StringBuffer each iteration. The silliness came from when people think using ("one" + "two" + "three" + "four") has the same problem. It does not have the same problem because the compiler knows that there are four concatenations, no intermediate results are being stored or used, and they are happen in the same expression. If you want performance and readability at the same time, my rule is this. If iterating, use a StringBuffer and append. If you have a series of concatenations, use the + since it is more readable. And use one or the other, don't mix.

Example:

  String str = new String();
  StringBuffer sb = new StringBuffer();  // good for loop appends
  for (int x = 0 ; x<10000 ; x++) {
     str.append("x is").append(x).append("."); 
  }


If you want performance and readability at the same time, my rule is this. If iterating, use a StringBuffer and append. If you have a series of concatenations, use the + since it is more readable.

I think have a better rule: Use whatever is the most readable. If performance is a problem, use a profiler to find out where and optimize that code. Chances are the performance issue won't have anything to do with concatenating strings.


I modified the source a bit:

public class StringAppendTest? {

private int numberOfRepetitions;
private String aWord = "One";

public StringAppendTest?(int numberOfRepetitions) { this.numberOfRepetitions = numberOfRepetitions; }

public void timingInLoops() { long start = System.currentTimeMillis(); String plusAppended = new String("");

for (int i = 0; i < numberOfRepetitions; i++) { plusAppended += aWord; }

System.out.println("Plus loop:"+(System.currentTimeMillis()-start)); System.out.println("plusAppended="+plusAppended.length());

start = System.currentTimeMillis();

StringBuffer buffer = new StringBuffer("");

for (int j = 0; j < numberOfRepetitions; j++) { buffer.append(aWord); }

System.out.println("Append loop:"+(System.currentTimeMillis()-start)); System.out.println("buffer="+buffer.length()); }

public void timingInCalls() { long start = System.currentTimeMillis();

String test =""; for (int i = 0; i < numberOfRepetitions; i++) { test = evaluatePlusAppend(test); } System.out.println("Plus calls:"+(System.currentTimeMillis()-start)); System.out.println("test="+test.length());

start = System.currentTimeMillis();

StringBuffer testBuf = new StringBuffer(); for (int j = 0; j < numberOfRepetitions; j++) { testBuf = evaluateBufferAppend(testBuf); } System.out.println("Append calls:"+(System.currentTimeMillis()-start)); System.out.println("testBuf="+testBuf.length());

}

private String evaluatePlusAppend(String original) { return original+aWord; }

private StringBuffer evaluateBufferAppend(StringBuffer buffer) { return buffer.append(aWord); }

public void run() { timingInLoops(); timingInCalls(); }

public static void main(String args[]) { StringAppendTest? test;

if (args.length>0 && args[0] != null) { test = new StringAppendTest?(Integer.parseInt(args[0])); } else { test = new StringAppendTest?(10000); }

test.run(); }
}

I get these results:

Plus loop:735

plusAppended=30000

Append loop:0

buffer=30000

Plus calls:719

test=30000

Append calls:15

testBuf=30000

Append is always faster, not that it matters. I was just bored.


EditText of this page (last edited June 30, 2006) or FindPage with title or text search