[PATCH] Allocate uninitialized byte[] to improve performance of String-related operations

Сергей Цыпанов sergei.tsypanov at yandex.ru
Fri Sep 4 12:52:32 UTC 2020


Hello,

currently jdk.internal.misc.Unsafe declares method allocateUninitializedArray(Class, int) returning uninitialized array of given type and length.

Allocation of uninitialized arrays is faster especially for larger ones, so we could use them for cases
when we are sure that created array will be completely overwritten or is guarded by count field (e.g. in AbstractStringBuilder).

I've exposed jdk.internal.misc.Unsafe.allocateUninitializedArray(Class, int)
via delegating method of sun.misc.Unsafe to measure creation of byte[] with benchmark [1]
and got those results:

                    (length)  Mode  Cnt      Score      Error   Units
constructor                0  avgt   50      7.639 ±    0.629   ns/op
constructor               10  avgt   50      9.448 ±    0.725   ns/op
constructor              100  avgt   50     21.158 ±    1.865   ns/op
constructor             1000  avgt   50    146.158 ±    9.836   ns/op
constructor            10000  avgt   50    916.321 ±   50.618   ns/op

unsafe                     0  avgt   50      8.057 ±    0.599   ns/op
unsafe                    10  avgt   50      8.308 ±    0.907   ns/op
unsafe                   100  avgt   50     12.232 ±    1.813   ns/op
unsafe                  1000  avgt   50     37.679 ±    1.382   ns/op
unsafe                 10000  avgt   50     78.896 ±    6.758   ns/op

As a result I propose to add the following methods into StringConcatHelper

@ForceInline
static byte[] newArray(int length) {
    return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, length);
}

@ForceInline
static char[] newCharArray(int length) {
    return (char[]) UNSAFE.allocateUninitializedArray(char.class, length);
}

along with existing StringConcatHelper.newArray(long indexCoder) and utilize them in String-related operations
instead of conventional array creation with new-keyword.

I've used benchmark [2] to measure impact on affected String-methods and found quite a good improvement:

                                                 before             after

Benchmark                   (length)      Score   Error     Score   Error    Units

newStringBuilderWithLength         8      8.288 ± 0.411    5.656 ±  0.019    ns/op
newStringBuilderWithLength        64     12.954 ± 0.687    7.588 ±  0.009    ns/op
newStringBuilderWithLength       128     20.603 ± 0.412   10.446 ±  0.005    ns/op
newStringBuilderWithLength      1024    119.935 ± 2.383   35.452 ±  0.029    ns/op

newStringBuilderWithString         8     19.721 ± 0.375   14.642 ±  0.052    ns/op
newStringBuilderWithString        64     34.006 ± 1.523   15.479 ±  0.031    ns/op
newStringBuilderWithString       128     36.697 ± 0.972   17.052 ±  0.133    ns/op
newStringBuilderWithString      1024    140.486 ± 6.396   85.156 ±  0.175    ns/op

repeatOneByteString                8     11.340 ± 0.197    9.736 ±  0.051    ns/op
repeatOneByteString               64     20.859 ± 0.257   15.073 ±  0.024    ns/op
repeatOneByteString              128     36.311 ± 1.162   22.808 ±  0.198    ns/op
repeatOneByteString             1024    149.243 ± 3.083   82.839 ±  0.193    ns/op

repeatOneChar                      8     28.033 ± 0.615   20.377 ±  0.137    ns/op
repeatOneChar                     64     56.399 ± 1.094   36.230 ±  0.051    ns/op
repeatOneChar                    128     68.423 ± 5.647   44.157 ±  0.239    ns/op
repeatOneChar                   1024    230.115 ± 0.312  179.126 ±  0.437    ns/op

replace                            8     14.684 ± 0.088   14.434 ±  0.057    ns/op
replace                           64     56.811 ± 0.612   56.420 ±  0.050    ns/op
replace                          128    112.694 ± 0.404  109.799 ±  1.202    ns/op
replace                         1024    837.939 ± 0.855  818.802 ±  0.408    ns/op

replaceUtf                         8     17.802 ± 0.074   15.744 ±  0.094    ns/op
replaceUtf                        64     45.754 ± 0.139   39.228 ±  0.864    ns/op
replaceUtf                       128     67.170 ± 0.353   50.497 ±  1.218    ns/op
replaceUtf                      1024    415.767 ± 6.829  297.831 ± 22.510    ns/op

toCharArray                        8      6.164 ± 0.033    6.128 ±  0.064    ns/op
toCharArray                       64     10.960 ± 0.032   13.566 ±  0.802    ns/op
toCharArray                      128     20.373 ± 0.013   20.823 ±  0.376    ns/op
toCharArray                     1024    165.923 ± 0.098  164.362 ±  0.065    ns/op

toCharArrayUTF                     8      8.009 ± 0.067    7.778 ±  0.026    ns/op
toCharArrayUTF                    64     11.112 ± 0.014   10.880 ±  0.010    ns/op
toCharArrayUTF                   128     20.390 ± 0.014   20.103 ±  0.017    ns/op
toCharArrayUTF                  1024    166.233 ± 0.076  163.827 ±  0.099    ns/op

So the question is could we include the changes of attached patch into JDK?

With best regards,
Sergey Tsypanov


1. Benchmark for array-allocation

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
public class ArrayAllocationBenchmark {
  private static Unsafe U;

  static {
    try {
      Field f = Unsafe.class.getDeclaredField("theUnsafe");
      f.setAccessible(true);
      U = (Unsafe) f.get(null);
    } catch (Exception e) {
      new RuntimeException(e);
    }
  }

  @Benchmark
  public byte[] constructor(Data data) {
    return new byte[data.length];
  }

  @Benchmark
  public byte[] unsafe(Data data) {
    return (byte[]) U.allocateUninitializedArray(byte.class, data.length);
  }

  @State(Scope.Thread)
  public static class Data {
    @Param({"0", "10", "100", "1000", "10000"})
    private int length;
  }
}


2. Benchmark for String-method measurements

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

  @Benchmark
  public char[] toCharArrayLatin1(Data data) {
    return data.string.toCharArray();
  }

  @Benchmark
  public char[] toCharArrayUTF(Data data) {
    return data.utfString.toCharArray();
  }

  @Benchmark
  public String repeatOneByteString(Data data) {
    return data.oneCharString.repeat(data.length);
  }

  @Benchmark
  public String repeatOneChar(Data data) {
    return data.oneUtfCharString.repeat(data.length);
  }

  @Benchmark
  public String replace(Data data){
    return data.stringToReplace.replace('z', 'b');
  }

  @Benchmark
  public String replaceUtf(Data data){
    return data.utfStringToReplace.replace('я', 'ю');
  }

  @Benchmark
  public StringBuilder newStringBuilderWithLength(Data data) {
    return new StringBuilder(data.length);
  }

  @Benchmark
  public StringBuilder newStringBuilderWithString(Data data) {
    return new StringBuilder(data.string);
  }

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

    @Param({"8", "64", "128", "1024"})
    private int length;

    private String string;
    public String utfString;
    private final String oneCharString = "a";
    private final String oneUtfCharString = "ё";
    private String stringToReplace;
    private String utfStringToReplace;

    @Setup
    public void setup() {
      string = oneCharString.repeat(length);
      utfString = oneUtfCharString.repeat(length);

      stringToReplace = string + 'z';
      utfStringToReplace = utfString + 'я';
    }
  }
}
-------------- next part --------------
A non-text attachment was scrubbed...
Name: uninit_array.patch
Type: text/x-diff
Size: 7416 bytes
Desc: not available
URL: <https://mail.openjdk.java.net/pipermail/core-libs-dev/attachments/20200904/57bd968e/uninit_array-0001.patch>


More information about the core-libs-dev mailing list