[PATCH] Performance improvement of StringBuilder.append(CharSerquence, int, int)

Сергей Цыпанов sergei.tsypanov at yandex.ru
Thu Jan 10 19:05:03 UTC 2019


Hi,

while investigating an issue I've found out that performance of expression (1) can be significantly improved by turning it into (2)

----------------------------------------------------------------------------------------------------

     //str is an instance of java.lang.String

(1) return new StringBuilder().append(str, beginIndex, endIndex).toString();

---->

(2) String substring = str.substring(beginIndex, endIndex);

     return new StringBuilder().append(substring).toString();

----------------------------------------------------------------------------------------------------

Using benchmark [1] I've got the following results:

Benchmark                                                                  (length)  Mode  Cnt     Score     Error   Units
StringBuilderAppendBenchmark.appendBounds                                        10  avgt   20    21,161 ±   0,545   ns/op
StringBuilderAppendBenchmark.appendBounds                                       100  avgt   20    72,230 ±   3,452   ns/op
StringBuilderAppendBenchmark.appendBounds                                      1000  avgt   20   476,256 ±  12,044   ns/op

StringBuilderAppendBenchmark.appendSubString                                     10  avgt   20    26,023 ±   0,871   ns/op
StringBuilderAppendBenchmark.appendSubString                                    100  avgt   20    38,824 ±   1,185   ns/op
StringBuilderAppendBenchmark.appendSubString                                   1000  avgt   20   214,072 ±   3,372   ns/op

StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm                    10  avgt   20    80,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm                   100  avgt   20   320,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm                  1000  avgt   20  2120,001 ±   0,001    B/op

StringBuilderAppendBenchmark.appendSubString:·gc.alloc.rate.norm                 10  avgt   20    96,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendSubString:·gc.alloc.rate.norm                100  avgt   20   192,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendSubString:·gc.alloc.rate.norm               1000  avgt   20  1088,000 ±   0,001    B/op


The reason for such a difference seems to be @HotSpotIntrinsicCandidate over StringBuilder.append(String),
while StringBuilder.append(CharSerquence, int, int) copies chars/bytes one by one.

I've prepared a patch [2] and applied it to fresh sources of JDK 11.
After rebuild the same benchmark demonstrated severe performance degradation:

- before patch

Benchmark                                                               (length)  Mode  Cnt     Score     Error   Units
StringBuilderAppendBenchmark.appendBounds                                     10  avgt   20    21,592 ±   0,763   ns/op
StringBuilderAppendBenchmark.appendBounds                                    100  avgt   20    72,241 ±   2,799   ns/op
StringBuilderAppendBenchmark.appendBounds                                   1000  avgt   20   488,106 ±  43,042   ns/op

StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm                 10  avgt   20    80,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm                100  avgt   20   320,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm               1000  avgt   20  2132,001 ±  10,691    B/op

- after patch

Benchmark                                                               (length)  Mode  Cnt     Score     Error   Units
StringBuilderAppendBenchmark.appendBounds                                     10  avgt   20    40,186 ±   1,828   ns/op
StringBuilderAppendBenchmark.appendBounds                                    100  avgt   20    89,031 ±   6,664   ns/op
StringBuilderAppendBenchmark.appendBounds                                   1000  avgt   20   669,934 ±  76,888   ns/op

StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm                 10  avgt   20   152,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm                100  avgt   20   440,000 ±   0,001    B/op
StringBuilderAppendBenchmark.appendBounds:·gc.alloc.rate.norm               1000  avgt   20  2688,001 ±   0,002    B/op

As I see the reason of degradation is that patched code now dodges intrinsified StringBuilder.append(String) and calls directly
AbstractStringBuilder.append(String) which in spite of utilizing System::arraycopy performes worse than intrinsic.

So my question is should I rework the patch somehow to make it perform better or I need to apply the transformation mentioned above ad hoc?


Appendix:

1) benchmark

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
public class StringBuilderAppendBenchmark {

  @Benchmark
  @SuppressWarnings("StringBufferReplaceableByString")
  public String appendSubString(Data data) {
    String str = data.str;
    int beginIndex = data.beginIndex;
    int endIndex = data.endIndex;

    String substring = str.substring(beginIndex, endIndex);

    return new StringBuilder().append('L').append(substring).append(';').toString();
  }

  @Benchmark
  public String appendBounds(Data data) {
    String str = data.str;
    int beginIndex = data.beginIndex;
    int endIndex = data.endIndex;

    return new StringBuilder().append('L').append(str, beginIndex, endIndex).append(';').toString();
  }

  @State(Scope.Thread)
  public static class Data {
    String str;

    @Param({"10", "100", "1000"})
    private int length;

    private int beginIndex;
    private int endIndex;

    @Setup
    public void setup() {
      str = randomString();
      beginIndex = length / 4;
      endIndex = length / 4* 3;
    }

    private String randomString() {
      ThreadLocalRandom random = ThreadLocalRandom.current();
      char[] chars = "abcdefghijklmnopqrstuvwxyz".toCharArray();

      StringBuilder sb = new StringBuilder(length);
      for (int i = 0; i < length; i++) {
        char c = chars[random.nextInt(chars.length)];
        sb.append(c);
      }
      return sb.toString();
    }
  }

}


2) patch

diff --git a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java
--- a/src/java.base/share/classes/java/lang/AbstractStringBuilder.java
+++ b/src/java.base/share/classes/java/lang/AbstractStringBuilder.java
@@ -626,10 +626,7 @@
             s = "null";
         }
         checkRange(start, end, s.length());
-        int len = end - start;
-        ensureCapacityInternal(count + len);
-        appendChars(s, start, end);
-        return this;
+        return append(s.subSequence(start, end).toString());
     }
 
     /**
@@ -1686,27 +1683,6 @@
         this.count = count + end - off;
     }
 
-    private final void appendChars(CharSequence s, int off, int end) {
-        if (isLatin1()) {
-            byte[] val = this.value;
-            for (int i = off, j = count; i < end; i++) {
-                char c = s.charAt(i);
-                if (StringLatin1.canEncode(c)) {
-                    val[j++] = (byte)c;
-                } else {
-                    count = j;
-                    inflate();
-                    StringUTF16.putCharsSB(this.value, j, s, i, end);
-                    count += end - i;
-                    return;
-                }
-            }
-        } else {
-            StringUTF16.putCharsSB(this.value, count, s, off, end);
-        }
-        count += end - off;
-    }
-
     /* IndexOutOfBoundsException, if out of bounds */
     private static void checkRange(int start, int end, int len) {
         if (start < 0 || start > end || end > len) {





More information about the core-libs-dev mailing list