0

I have tried this example to find the different in time of execution for StringBuffer, StringBuilder and String

After tried, i get to know that StringBuffer and StringBuilder taken less time, because its not creating new objects.

As a String append with empty string also not creating any object so its faster.

When I am doing append with some string, it should take more time because it takes time for object creation.

When I am doing same append string pattern with another string that time also taking more time. In this case all object are already available in String pool. Why its taking same time as before?

public class StringComparation {
  public static void main(String[] args) {
    int N = 100000;
    long time;
    // String Buffer
    StringBuffer sb = new StringBuffer();
    time = System.currentTimeMillis();
    for (int i = N; i --> 0 ;) {
         sb.append("a");
    }
    System.out.println("String Buffer - " + (System.currentTimeMillis() - time));
    // String Builder 
    StringBuilder sbr = new StringBuilder();
    time = System.currentTimeMillis();
    for (int i = N; i --> 0 ;) {
        sbr.append("a");
    }
    System.out.println("String Builder - " + (System.currentTimeMillis() - time));
    // String Without String pool value 
    String s2 = new String();
    time = System.currentTimeMillis();
    for (int i = N; i --> 0 ;) {
        s2 = s2 + "";
    }
    System.out.println("String Without String pool value - " 
              + (System.currentTimeMillis() - time));
    // String With new String pool Object
    String s = new String();
    time = System.currentTimeMillis();
    for (int i = N; i --> 0 ;) {
        s = s + "a";
    }
    System.out.println("String With new String pool Object - " 
            + (System.currentTimeMillis() - time));
    // String With already available String pool Object 
    String s1 = new String();
    time = System.currentTimeMillis();
    for (int i = N; i --> 0 ;) {
        s1 = s1 + "a";
    }
    System.out.println("String With already available String pool Object - " 
            + (System.currentTimeMillis() - time));       
  }
}

Output :

String Buffer - 43
String Builder - 16
String Without String pool value - 64
String With new String pool Object - 12659
String With already available String pool Object - 14258

Kindly correct me if I am wrong anywhere.

R D
  • 1,330
  • 13
  • 30
  • Did you add a warmup phase prior to the test? – Hannes Aug 14 '14 at 10:40
  • Have a look at the compiled class. – Hannes Aug 14 '14 at 10:40
  • If any string pattern available in string pool, then same pattern wont create newly i am correct?. If so why last case take same time as newly creating string pool pattern. – R D Aug 14 '14 at 10:42
  • No, `new` **always** creates a new object. There's no difference between your last two cases, they're identical save for a variable name which makes no difference to the actual execution. – JonK Aug 14 '14 at 10:48
  • @JonK http://stackoverflow.com/questions/5192574/how-many-java-objects-are-generated-by-this-new-stringabcd http://stackoverflow.com/questions/14497335/how-many-objects-are-created-in-the-following-strings-in-java – R D Aug 14 '14 at 10:51
  • Neither of those are really relevant. The massive performance drop you're seeing in the last two cases has nothing to do with String pooling or interning, and everything to do with the fact that Java compilers are terrible at optimising looped String concatenations. These are exactly the results I would expect to see from your examples. – JonK Aug 14 '14 at 10:56

1 Answers1

1

Given that your last two tests are identical, you really only have four tests here. For the sake of convenience I have refactored them into separate methods and removed the benchmarking code as it's not necessary to understand what's going on here.

public static void stringBuilderTest(int iterations) {
    final StringBuilder sb = new StringBuilder();
    for (int i = iterations; i-- > 0;) {
        sb.append("a");
    }
}

public static void stringBufferTest(int iterations) {
    final StringBuffer sb = new StringBuffer();
    for (int i = iterations; i-- > 0;) {
        sb.append("a");
    }
}

public static void emptyStringConcatTest(int iterations) {
    String s = new String();
    for (int i = iterations; i-- > 0;) {
        s += "";
    }
}

public static void nonEmptyStringConcatTest(int iterations) {
    String s = new String();
    for (int i = iterations; i-- > 0;) {
        s += "a";
    }
}

We already know that the StringBuilder version of the code is the fastest of the four. The StringBuffer version is slower because all of its operations are synchronized, which carries an unavoidable overhead that StringBuilder doesn't have because it isn't synchronized.

So the two methods that we're interested in are emptyStringConcatTest and nonEmptyStringConcatTest. If we inspect the bytecode for the compiled version of emptyStringConcatTest, we see the following:

  public static void emptyStringConcatTest(int);
    flags: ACC_PUBLIC, ACC_STATIC
    LineNumberTable:
      line 27: 0
      line 28: 8
      line 29: 17
      line 31: 40
    Code:
      stack=2, locals=3, args_size=1
         0: new           #14                 // class java/lang/String
         3: dup
         4: invokespecial #15                 // Method java/lang/String."<init>":()V
         7: astore_1
         8: iload_0
         9: istore_2
        10: iload_2
        11: iinc          2, -1
        14: ifle          40
        17: new           #7                  // class java/lang/StringBuilder
        20: dup
        21: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
        24: aload_1
        25: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        28: ldc           #16                 // String
        30: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        33: invokevirtual #17                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        36: astore_1
        37: goto          10
        40: return
      LineNumberTable:
        line 27: 0
        line 28: 8
        line 29: 17
        line 31: 40
      StackMapTable: number_of_entries = 2
           frame_type = 253 /* append */
             offset_delta = 10
        locals = [ class java/lang/String, int ]
           frame_type = 250 /* chop */
          offset_delta = 29

Under the hood, the two methods are almost identical, with this line being the only difference:

Empty String:

28: ldc           #9                  // String 

Non-empty String (note the small but important difference!):

28: ldc           #9                  // String a

The first thing to note about the bytecode is the structure of the for loop's body:

10: iload_2
11: iinc          2, -1
14: ifle          40
17: new           #7                  // class java/lang/StringBuilder
20: dup
21: invokespecial #8                  // Method java/lang/StringBuilder."<init>":()V
24: aload_1
25: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
28: ldc           #16                 // String
30: invokevirtual #10                 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
33: invokevirtual #17                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
36: astore_1
37: goto          10

What we've actually ended up with here is a compiler optimisation that's turned

for (int i = iterations; i-- > 0;) {
    s += "";
}

into:

for (int i = iterations; i-- > 0;) {
    s = new StringBuilder().append(s).append("").toString();
}

That's not good. We're instantiating a new temporary StringBuilder object in every single iteration, of which there are 100,000. That's a lot of objects.

The difference you see between the emptyStringConcatTest and nonEmptyStringConcatTest can be further explained if we inspect the source code of StringBuilder#append(String):

public StringBuilder append(String str) {
    super.append(str);
    return this;
}

The superclass of StringBuilder is AbstractStringBuilder, so let's take a look at its implementation of append(String):

public AbstractStringBuilder append(String str) {
    if (str == null) str = "null";
        int len = str.length();
    if (len == 0) return this;
    int newCount = count + len;
    if (newCount > value.length)
        expandCapacity(newCount);
    str.getChars(0, len, value, count);
    count = newCount;
    return this;
}

You'll notice here that if the length of the parameter str is zero, the method simply returns without doing any further operations, making it pretty quick in the case of an empty String.

Non-empty String parameters trigger bounds-checking of the backing char[], potentially causing it to be resized by expandCapacity(int), which copies the original array into a new, larger array (note that the backing array in a StringBuilder is not final - it can be reassigned!). Once that's done, we call out to String#getChars(int, int, char[], int), which does more array copying. The exact implementations of the array copying are hidden away in native code so I'm not going to dig around to find them.

To further compound that, the sheer number of objects that we're creating and then throwing away may be sufficient to trigger a run of the JVM's Garbage Collector, which carries a further overhead with it.

So in summary; the huge drop in performance for your equivalent to nonEmptyStringConcatTest is largely down to an awful 'optimisation' that the compiler made. Avoid it by never doing direct concatenation inside a loop.

JonK
  • 2,097
  • 2
  • 25
  • 36