Thursday, May 29, 2008

String, StringBuffer, and StringBuilder: There Is A Performance Difference

A recent posting on the New to Java forum served as a reminder of how the immutable Java String can significantly impact performance when used in a situation where a mutable StringBuffer or StringBuilder would be more appropriate (such as String concatenation inside a loop). The original poster's application that generated a web page from a database query resultset improved from ten minutes to less than three seconds when two strategic uses of String were replaced with StringBuffer. The poster also reported that an application that previously ran for over 30 minutes was running in fewer than 7 seconds with a similar String-to-StringBuffer change.

I have been cognizant of the difference between String and StringBuffer for some time, but it is easy to forget how significant of an impact the incorrect use of the immutable String can have until you use it or see it used incorrectly. I decided to play with String, StringBuffer, and StringBuilder to see how dramatic the difference really is.

The following code listing shows a simple Java class that pits String, StringBuffer, and StringBuilder in a contest that an immutable String really has no business being in.


import java.util.ArrayList;
import java.util.List;

/**
* Simple class testing the rough differences in performance of String,
* StringBuffer, and StringBuilder in repeated (looping) String concatenations.
*
* @see java.lang.String
* @see java.lang.StringBuffer
* @see java.lang.StringBuilder
*/
public class StringPerformanceTester
{
final private String CONSTANT_STRING;

/**
* Constructor accepting the String to be used for appending or concatenating
* to the main String. The ability to specify this String provides additional
* flexibility in the performance testing.
*/
public StringPerformanceTester(final String stringToBeUsed)
{
CONSTANT_STRING = stringToBeUsed;
}

/**
* Concatenate Strings repeatedly the specified number of times.
*
* @param numberOfStrings Number of times to concatenate Strings.
* @return Time in milliseconds required to concatenate Strings.
*/
public long concatenateStrings(final int numberOfStrings)
{
String masterString = "";
final long startTime = System.currentTimeMillis();
for ( int i=0; i < numberOfStrings; ++i )
{
masterString += CONSTANT_STRING;
}
final long endTime = System.currentTimeMillis();
return endTime - startTime;
}

/**
* Concatenate Strings repeatedly the specified number of times using
* StringBuffer.
*
* @param numberOfStrings Number of times to append Strings.
* @return Time in milliseconds required to append Strings.
*/
public long useStringBuffer(final int numberOfStrings)
{
final StringBuffer masterString =
new StringBuffer(CONSTANT_STRING.length()*numberOfStrings);
final long startTime = System.currentTimeMillis();
for ( int i=0; i < numberOfStrings; ++i )
{
masterString.append(CONSTANT_STRING);
}
final long endTime = System.currentTimeMillis();
return endTime - startTime;
}

/**
* Concatenate Strings repeatedly the specified number of times using
* StringBuilder.
*
* @param numberOfStrings Number of times to append Strings.
* @return Time in milliseconds required to append Strings.
*/
public long useStringBuilder(final int numberOfStrings)
{
final StringBuilder masterString =
new StringBuilder(CONSTANT_STRING.length()*numberOfStrings);
final long startTime = System.currentTimeMillis();
for ( int i=0; i < numberOfStrings; ++i )
{
masterString.append(CONSTANT_STRING);
}
final long endTime = System.currentTimeMillis();
return endTime - startTime;
}

/**
* Main method for executing timing tests of different methods of String
* concatenation.
*
* @param numberOfStringsForTest Number of strings to be concatenated or appended.
* @return Results of running all three types of tests against provided
* number of Strings.
*/
public TestResults runTimingTests(final int numberOfStringsForTest)
{
final TestResults testResults =
this.new TestResults(numberOfStringsForTest);
testResults.millisecondsString = concatenateStrings(numberOfStringsForTest);
testResults.millisecondsBuffer = useStringBuffer(numberOfStringsForTest);
testResults.millisecondsBuilder = useStringBuilder(numberOfStringsForTest);
return testResults;
}

/**
* Main executable for testing String performance.
*/
public static void main(final String[] arguments)
{
final StringPerformanceTester me = new StringPerformanceTester("constant");
final List<TestResults> testResults = new ArrayList<TestResults>();

testResults.add( me.runTimingTests(1000) );
testResults.add( me.runTimingTests(10000) );
testResults.add( me.runTimingTests(100000) );
System.out.println(testResults);
}

/**
* Class for easy storage and output of test results.
*/
private class TestResults
{
private int numberOfStrings;
private long millisecondsString;
private long millisecondsBuffer;
private long millisecondsBuilder;

/**
* Constructor accepting the number of String concatenations or appends.
*
* @param numberOfStrings Number of String concatenations or appends.
*/
public TestResults(final int numberOfStrings)
{
this.numberOfStrings = numberOfStrings;
}

/**
* Provide String representation of me.
*
* @return String representation of me.
*/
@Override
public String toString()
{
final StringBuffer sb = new StringBuffer();
sb.append("#: " + numberOfStrings);
sb.append("\tSTRING: " + millisecondsString);
sb.append("\tBUFFER: " + millisecondsBuffer);
sb.append("\tBUILDER: " + millisecondsBuilder + "\n");
return sb.toString();
}
}
}


When the test above is run, the results are dramatic. The largest test run involved concatenating the same String one hundred thousand times using String's += operator and the append method of the StringBuilder and StringBuffer classes. The results are so striking that the String's results for 100,000 concatenations is most easily measured in minutes while the StringBuffer and StringBuffer accomplish the same concatenation in time most easily measured in miliiseconds.

The following screen snapshot shows the results of the run.



For performance tests and benchmark runs to be useful, many runs of the same code under carefully controlled circumstances are often required to get accurate results. However, in this simple case, the sheer magnitude of the performance difference implies that even variables introduced by too few of runs in too loosely controlled environments cannot be explained by these inconsistencies.

There are several things one could try in conjunction with these tests for further measurements. For example, I took advantage of knowing the size of my StringBuffer and StringBuilder at instantiation time. If I started with smaller sizes and forced them to expand as needed, I might see a little worse performance. I could also try running with the -server option and could try different sizes of Strings and different number of Strings.

One lesson I think it pretty clear from this is that when frequent String concatenation is required (especially in loops), StringBuffer is easily preferable to String. I'll generally favor StringBuffer in such situations and only use the unsynchronized StringBuilder if I need to squeeze every last millisecond out and am in a single-thread situation. Otherwise, the synchronized StringBuffer competes well enough with the unsynchronized StringBuilder that I prefer StringBuffer for general String concatenation needs.

There are several good resources that explain the difference between String, StringBuffer, and StringBuilder. A concise overview of the differences and when to use each is provided in Java Tip - Difference Between String, StringBuffer, and StringBuilder. A useful blog comparing these is Difference Between String and StringBuffer/StringBuilder in Java. A satirical look at String versus StringBuffer is available in Maximize Java Performance by Avoiding StringBuffer (some of its feedback comments are interesting as well). Another worthwhile blog entry is StringBuffer/StringBuilder Performance Improvements.




UPDATE (18 August 2008): Another interesting resource on Strings is the PMD (PMD doesn't stand for anything) String and StringBuffer Rules description.

UPDATE (25 August 2008): See the blog entry Don't Use StringBuffer! for a compelling post on why StringBuilder is almost always preferable to StringBuffer.

UPDATE (15 January 2010): See the blog post About String Concatenation in Java or 'don’t fear the +' for additional focus on the differentiation between a single statement and multiple statements using + operator.

1 comment:

Aashish said...

String is an immutable class while StringBuilder & StringBuffer are mutable classes. StringBuffer is synchronized while StringBuilder is not.

Below link can be useful to find more differences between String, StringBuffer & StringBuilder

Difference between String, StringBuffer and StringBuilder

http://newtechnobuzzz.blogspot.com/2014/07/difference-between-string-stringbuffer.html