diff --git a/src/java.base/share/classes/java/lang/StringConcatHelper.java b/src/java.base/share/classes/java/lang/StringConcatHelper.java
index 486e115369e..ae2b9693409 100644
--- a/src/java.base/share/classes/java/lang/StringConcatHelper.java
+++ b/src/java.base/share/classes/java/lang/StringConcatHelper.java
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -28,6 +29,7 @@ package java.lang;
import jdk.internal.misc.Unsafe;
import jdk.internal.util.DecimalDigits;
import jdk.internal.vm.annotation.ForceInline;
+import jdk.internal.vm.annotation.Stable;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
@@ -39,6 +41,98 @@ import java.lang.invoke.MethodType;
* combinators there.
*/
final class StringConcatHelper {
+ static abstract class StringConcatBase {
+ @Stable
+ final String[] constants;
+ final int length;
+ final byte coder;
+
+ StringConcatBase(String[] constants) {
+ int length = 0;
+ byte coder = String.LATIN1;
+ for (String c : constants) {
+ length += c.length();
+ coder |= c.coder();
+ }
+ this.constants = constants;
+ this.length = length;
+ this.coder = coder;
+ }
+ }
+
+ static final class Concat1 extends StringConcatBase {
+ Concat1(String[] constants) {
+ super(constants);
+ }
+
+ @ForceInline
+ String concat0(String value) {
+ int length = stringSize(this.length, value);
+ byte coder = (byte) (this.coder | value.coder());
+ byte[] buf = newArray(length << coder);
+ String prefix = constants[0];
+ prefix.getBytes(buf, 0, coder);
+ value.getBytes(buf, prefix.length(), coder);
+ constants[1].getBytes(buf, prefix.length() + value.length(), coder);
+ return new String(buf, coder);
+ }
+
+ @ForceInline
+ String concat(boolean value) {
+ int length = stringSize(this.length, value);
+ String suffix = constants[1];
+ length -= suffix.length();
+ byte[] buf = newArrayWithSuffix(suffix, length, coder);
+ prepend(length, coder, buf, value, constants[0]);
+ return new String(buf, coder);
+ }
+
+ @ForceInline
+ String concat(char value) {
+ int length = stringSize(this.length, value);
+ byte coder = (byte) (this.coder | stringCoder(value));
+ String suffix = constants[1];
+ length -= suffix.length();
+ byte[] buf = newArrayWithSuffix(suffix, length, coder);
+ prepend(length, coder, buf, value, constants[0]);
+ return new String(buf, coder);
+ }
+
+ @ForceInline
+ String concat(int value) {
+ int length = stringSize(this.length, value);
+ String suffix = constants[1];
+ length -= suffix.length();
+ byte[] buf = newArrayWithSuffix(suffix, length, coder);
+ prepend(length, coder, buf, value, constants[0]);
+ return new String(buf, coder);
+ }
+
+ @ForceInline
+ String concat(long value) {
+ int length = stringSize(this.length, value);
+ String suffix = constants[1];
+ length -= suffix.length();
+ byte[] buf = newArrayWithSuffix(suffix, length, coder);
+ prepend(length, coder, buf, value, constants[0]);
+ return new String(buf, coder);
+ }
+
+ @ForceInline
+ String concat(Object value) {
+ return concat0(stringOf(value));
+ }
+
+ @ForceInline
+ String concat(float value) {
+ return concat0(Float.toString(value));
+ }
+
+ @ForceInline
+ String concat(double value) {
+ return concat0(Double.toString(value));
+ }
+ }
private StringConcatHelper() {
// no instantiation
@@ -375,6 +469,64 @@ final class StringConcatHelper {
private static final Unsafe UNSAFE = Unsafe.getUnsafe();
+ static String stringOf(float value) {
+ return Float.toString(value);
+ }
+
+ static String stringOf(double value) {
+ return Double.toString(value);
+ }
+
+ /**
+ * return add stringSize of value
+ * @param length length
+ * @param value value to add stringSize
+ * @return new length
+ */
+ static int stringSize(int length, char value) {
+ return checkOverflow(length + 1);
+ }
+
+ /**
+ * return add stringSize of value
+ * @param length length
+ * @param value value to add stringSize
+ * @return new length
+ */
+ static int stringSize(int length, boolean value) {
+ return checkOverflow(length + (value ? 4 : 5));
+ }
+
+ /**
+ * return add stringSize of value
+ * @param length length
+ * @param value value
+ * @return new length
+ */
+ static int stringSize(int length, int value) {
+ return checkOverflow(length + DecimalDigits.stringSize(value));
+ }
+
+ /**
+ * return add stringSize of value
+ * @param length length
+ * @param value value to add stringSize
+ * @return new length
+ */
+ static int stringSize(int length, long value) {
+ return checkOverflow(length + DecimalDigits.stringSize(value));
+ }
+
+ /**
+ * return add stringSize of value
+ * @param length length
+ * @param value value to add stringSize
+ * @return new length
+ */
+ static int stringSize(int length, String value) {
+ return checkOverflow(length + value.length());
+ }
+
/**
* Allocates an uninitialized byte array based on the length and coder
* information, then prepends the given suffix string at the end of the
@@ -440,4 +592,195 @@ final class StringConcatHelper {
}
}
+ /**
+ * Allocates an uninitialized byte array based on the length and coder
+ * information, then prepends the given suffix string at the end of the
+ * byte array before returning it. The calling code must adjust the
+ * indexCoder so that it's taken the coder of the suffix into account, but
+ * subtracted the length of the suffix.
+ *
+ * @param suffix
+ * @param indexCoder
+ * @return the newly allocated byte array
+ */
+ @ForceInline
+ static byte[] newArrayWithSuffix(String suffix, int index, byte coder) {
+ byte[] buf = newArray((index + suffix.length()) << coder);
+ if (coder == String.LATIN1) {
+ suffix.getBytes(buf, index, String.LATIN1);
+ } else {
+ suffix.getBytes(buf, index, String.UTF16);
+ }
+ return buf;
+ }
+
+ /**
+ * Return the coder for the character.
+ * @param value character
+ * @return coder
+ */
+ static byte stringCoder(char value) {
+ return StringLatin1.canEncode(value) ? String.LATIN1 : String.UTF16;
+ }
+
+ /**
+ * Prepends constant and the stringly representation of value into buffer,
+ * given the coder and final index. Index is measured in chars, not in bytes!
+ *
+ * @param index final char index in the buffer
+ * @param coder coder of the buffer
+ * @param buf buffer to append to
+ * @param value boolean value to encode
+ * @param prefix a constant to prepend before value
+ * @return updated index
+ */
+ static int prepend(int index, byte coder, byte[] buf, boolean value, String prefix) {
+ if (coder == String.LATIN1) {
+ if (value) {
+ index -= 4;
+ buf[index] = 't';
+ buf[index + 1] = 'r';
+ buf[index + 2] = 'u';
+ buf[index + 3] = 'e';
+ } else {
+ index -= 5;
+ buf[index] = 'f';
+ buf[index + 1] = 'a';
+ buf[index + 2] = 'l';
+ buf[index + 3] = 's';
+ buf[index + 4] = 'e';
+ }
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.LATIN1);
+ } else {
+ if (value) {
+ index -= 4;
+ StringUTF16.putChar(buf, index, 't');
+ StringUTF16.putChar(buf, index + 1, 'r');
+ StringUTF16.putChar(buf, index + 2, 'u');
+ StringUTF16.putChar(buf, index + 3, 'e');
+ } else {
+ index -= 5;
+ StringUTF16.putChar(buf, index, 'f');
+ StringUTF16.putChar(buf, index + 1, 'a');
+ StringUTF16.putChar(buf, index + 2, 'l');
+ StringUTF16.putChar(buf, index + 3, 's');
+ StringUTF16.putChar(buf, index + 4, 'e');
+ }
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.UTF16);
+ }
+ return index;
+ }
+
+ /**
+ * Prepends constant and the stringly representation of value into buffer,
+ * given the coder and final index. Index is measured in chars, not in bytes!
+ *
+ * @param index final char index in the buffer
+ * @param coder coder of the buffer
+ * @param buf buffer to append to
+ * @param value char value to encode
+ * @param prefix a constant to prepend before value
+ * @return updated index
+ */
+ static int prepend(int index, byte coder, byte[] buf, char value, String prefix) {
+ if (coder == String.LATIN1) {
+ buf[--index] = (byte) (value & 0xFF);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.LATIN1);
+ } else {
+ StringUTF16.putChar(buf, --index, value);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.UTF16);
+ }
+ return index;
+ }
+
+ /**
+ * Prepends constant and the stringly representation of value into buffer,
+ * given the coder and final index. Index is measured in chars, not in bytes!
+ *
+ * @param index final char index in the buffer
+ * @param coder coder of the buffer
+ * @param buf buffer to append to
+ * @param value int value to encode
+ * @param prefix a constant to prepend before value
+ * @return updated index
+ */
+ static int prepend(int index, byte coder, byte[] buf, int value, String prefix) {
+ if (coder == String.LATIN1) {
+ index = StringLatin1.getChars(value, index, buf);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.LATIN1);
+ } else {
+ index = StringUTF16.getChars(value, index, buf);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.UTF16);
+ }
+ return index;
+ }
+
+ /**
+ * Prepends constant and the stringly representation of value into buffer,
+ * given the coder and final index. Index is measured in chars, not in bytes!
+ *
+ * @param index final char index in the buffer
+ * @param coder coder of the buffer
+ * @param buf buffer to append to
+ * @param value long value to encode
+ * @param prefix a constant to prepend before value
+ * @return updated index
+ */
+ static int prepend(int index, byte coder, byte[] buf, long value, String prefix) {
+ if (coder == String.LATIN1) {
+ index = StringLatin1.getChars(value, index, buf);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.LATIN1);
+ } else {
+ index = StringUTF16.getChars(value, index, buf);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.UTF16);
+ }
+ return index;
+ }
+
+ /**
+ * Prepends constant and the stringly representation of value into buffer,
+ * given the coder and final index. Index is measured in chars, not in bytes!
+ *
+ * @param index final char index in the buffer
+ * @param coder coder of the buffer
+ * @param buf buffer to append to
+ * @param value boolean value to encode
+ * @param prefix a constant to prepend before value
+ * @return updated index
+ */
+ static int prepend(int index, byte coder, byte[] buf, String value, String prefix) {
+ index -= value.length();
+ if (coder == String.LATIN1) {
+ value.getBytes(buf, index, String.LATIN1);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.LATIN1);
+ } else {
+ value.getBytes(buf, index, String.UTF16);
+ index -= prefix.length();
+ prefix.getBytes(buf, index, String.UTF16);
+ }
+ return index;
+ }
+
+ /**
+ * Check for overflow, throw exception on overflow.
+ *
+ * @param value
+ * @return the given parameter value, if valid
+ */
+ @ForceInline
+ static int checkOverflow(int value) {
+ if (value >= 0) {
+ return value;
+ }
+ throw new OutOfMemoryError("Overflow: String length out of range");
+ }
}
diff --git a/src/java.base/share/classes/java/lang/System.java b/src/java.base/share/classes/java/lang/System.java
index 0947da8ded7..5ff4796505b 100644
--- a/src/java.base/share/classes/java/lang/System.java
+++ b/src/java.base/share/classes/java/lang/System.java
@@ -2623,6 +2623,10 @@ public final class System {
return StringConcatHelper.mix(lengthCoder, value);
}
+ public Object stringConcat1(String[] constants) {
+ return new StringConcatHelper.Concat1(constants);
+ }
+
public int getCharsLatin1(long i, int index, byte[] buf) {
return StringLatin1.getChars(i, index, buf);
}
diff --git a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
index cf552c434be..dd262193574 100644
--- a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
+++ b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2015, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -30,23 +31,33 @@ import jdk.internal.access.SharedSecrets;
import jdk.internal.constant.ConstantUtils;
import jdk.internal.misc.VM;
import jdk.internal.util.ClassFileDumper;
+import jdk.internal.util.ReferenceKey;
+import jdk.internal.util.ReferencedKeyMap;
import jdk.internal.vm.annotation.Stable;
import sun.invoke.util.Wrapper;
+import java.lang.classfile.Annotation;
import java.lang.classfile.ClassBuilder;
import java.lang.classfile.ClassFile;
import java.lang.classfile.CodeBuilder;
+import java.lang.classfile.MethodBuilder;
import java.lang.classfile.TypeKind;
+import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute;
import java.lang.constant.ClassDesc;
-import java.lang.constant.ConstantDescs;
import java.lang.constant.MethodTypeDesc;
import java.lang.invoke.MethodHandles.Lookup;
-import java.lang.reflect.AccessFlag;
+import java.lang.ref.SoftReference;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Consumer;
+import java.util.function.Supplier;
-import static java.lang.invoke.MethodHandles.Lookup.ClassOption.STRONG;
+import static java.lang.classfile.ClassFile.*;
+import static java.lang.constant.ConstantDescs.*;
import static java.lang.invoke.MethodType.methodType;
/**
@@ -107,12 +118,15 @@ import static java.lang.invoke.MethodType.methodType;
* @since 9
*/
public final class StringConcatFactory {
-
private static final int HIGH_ARITY_THRESHOLD;
+ private static final int FORCE_INLINE_THRESHOLD;
static {
String highArity = VM.getSavedProperty("java.lang.invoke.StringConcat.highArityThreshold");
- HIGH_ARITY_THRESHOLD = highArity != null ? Integer.parseInt(highArity) : 20;
+ HIGH_ARITY_THRESHOLD = highArity != null ? Integer.parseInt(highArity) : 0;
+
+ String inlineThreshold = VM.getSavedProperty("java.lang.invoke.StringConcat.inlineThreshold");
+ FORCE_INLINE_THRESHOLD = inlineThreshold != null ? Integer.parseInt(inlineThreshold) : 16;
}
/**
@@ -371,14 +385,17 @@ public final class StringConcatFactory {
}
try {
- if (concatType.parameterCount() <= HIGH_ARITY_THRESHOLD) {
- return new ConstantCallSite(
- generateMHInlineCopy(concatType, constantStrings)
- .viewAsType(concatType, true));
- } else {
- return new ConstantCallSite(
- SimpleStringBuilderStrategy.generate(lookup, concatType, constantStrings));
+ MethodHandle mh = makeSimpleConcat(concatType, constantStrings);
+ if (mh == null && concatType.parameterCount() <= HIGH_ARITY_THRESHOLD) {
+ mh = generateMHInlineCopy(concatType, constantStrings);
}
+
+ if (mh == null) {
+ mh = InlineHiddenClassStrategy.generate(lookup, concatType, constantStrings);
+ }
+ mh = mh.viewAsType(concatType, true);
+
+ return new ConstantCallSite(mh);
} catch (Error e) {
// Pass through any error
throw e;
@@ -427,7 +444,7 @@ public final class StringConcatFactory {
}
// Flush any accumulated characters into a constant
- consts[oCount++] = acc.length() > 0 ? acc.toString() : null;
+ consts[oCount++] = acc.length() > 0 ? acc.toString() : "";
acc.setLength(0);
} else {
// Not a special character, this is a constant embedded into
@@ -443,7 +460,7 @@ public final class StringConcatFactory {
}
// Flush the remaining characters as constant:
- consts[oCount] = acc.length() > 0 ? acc.toString() : null;
+ consts[oCount] = acc.length() > 0 ? acc.toString() : "";
return consts;
}
@@ -466,6 +483,36 @@ public final class StringConcatFactory {
" are passed");
}
+ private static MethodHandle makeSimpleConcat(MethodType mt, String[] constants) {
+ int paramCount = mt.parameterCount();
+ String suffix = constants[paramCount];
+
+ // Fast-path trivial concatenations
+ if (paramCount == 0) {
+ return MethodHandles.insertArguments(newStringifier(), 0, suffix == null ? "" : suffix);
+ }
+ if (paramCount == 1) {
+ String prefix = constants[0];
+ // Empty constants will be
+ if (prefix.isEmpty()) {
+ if (suffix.isEmpty()) {
+ return unaryConcat(mt.parameterType(0));
+ } else if (!mt.hasPrimitives()) {
+ return MethodHandles.insertArguments(simpleConcat(), 1, suffix);
+ } // else fall-through
+ } else if (suffix.isEmpty() && !mt.hasPrimitives()) {
+ // Non-primitive argument
+ return MethodHandles.insertArguments(simpleConcat(), 0, prefix);
+ } // fall-through if there's both a prefix and suffix
+ } else if (paramCount == 2 && !mt.hasPrimitives() && suffix.isEmpty()
+ && constants[0].isEmpty() && constants[1].isEmpty()) {
+ // Two reference arguments, no surrounding constants
+ return simpleConcat();
+ }
+
+ return null;
+ }
+
/**
*
This strategy replicates what StringBuilders are doing: it builds the
* byte[] array on its own and passes that byte[] array to String
@@ -477,29 +524,7 @@ public final class StringConcatFactory {
int paramCount = mt.parameterCount();
String suffix = constants[paramCount];
- // Fast-path trivial concatenations
- if (paramCount == 0) {
- return MethodHandles.insertArguments(newStringifier(), 0, suffix == null ? "" : suffix);
- }
- if (paramCount == 1) {
- String prefix = constants[0];
- // Empty constants will be
- if (prefix == null) {
- if (suffix == null) {
- return unaryConcat(mt.parameterType(0));
- } else if (!mt.hasPrimitives()) {
- return MethodHandles.insertArguments(simpleConcat(), 1, suffix);
- } // else fall-through
- } else if (suffix == null && !mt.hasPrimitives()) {
- // Non-primitive argument
- return MethodHandles.insertArguments(simpleConcat(), 0, prefix);
- } // fall-through if there's both a prefix and suffix
- }
- if (paramCount == 2 && !mt.hasPrimitives() && suffix == null
- && constants[0] == null && constants[1] == null) {
- // Two reference arguments, no surrounding constants
- return simpleConcat();
- }
+
// else... fall-through to slow-path
// Create filters and obtain filtered parameter types. Filters would be used in the beginning
@@ -1043,139 +1068,638 @@ public final class StringConcatFactory {
}
/**
- * Bytecode StringBuilder strategy.
+ * Implement efficient hidden class strategy for String concatenation
*
- *
This strategy emits StringBuilder chains as similar as possible
- * to what javac would. No exact sizing of parameters or estimates.
+ *
This strategy replicates based on the bytecode what StringBuilders are doing: it builds the
+ * byte[] array on its own and passes that byte[] array to String
+ * constructor. This strategy requires access to some private APIs in JDK,
+ * most notably, the private String constructor that accepts byte[] arrays
+ * without copying.
*/
- private static final class SimpleStringBuilderStrategy {
- static final String METHOD_NAME = "concat";
- static final ClassDesc STRING_BUILDER = ClassDesc.ofDescriptor("Ljava/lang/StringBuilder;");
+ private static final class InlineHiddenClassStrategy {
+ static final String CLASS_NAME = "java.lang.String$$StringConcat";
+ static final String METHOD_NAME = "concat";
+
static final ClassFileDumper DUMPER =
ClassFileDumper.getInstance("java.lang.invoke.StringConcatFactory.dump", "stringConcatClasses");
- static final MethodTypeDesc APPEND_BOOLEAN_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_boolean);
- static final MethodTypeDesc APPEND_CHAR_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_char);
- static final MethodTypeDesc APPEND_DOUBLE_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_double);
- static final MethodTypeDesc APPEND_FLOAT_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_float);
- static final MethodTypeDesc APPEND_INT_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_int);
- static final MethodTypeDesc APPEND_LONG_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_long);
- static final MethodTypeDesc APPEND_OBJECT_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_Object);
- static final MethodTypeDesc APPEND_STRING_TYPE = MethodTypeDesc.of(STRING_BUILDER, ConstantDescs.CD_String);
- static final MethodTypeDesc INT_CONSTRUCTOR_TYPE = MethodTypeDesc.of(ConstantDescs.CD_void, ConstantDescs.CD_int);
- static final MethodTypeDesc TO_STRING_TYPE = MethodTypeDesc.of(ConstantDescs.CD_String);
+ static final MethodHandles.Lookup STR_LOOKUP = new MethodHandles.Lookup(String.class);
- /**
- * Ensure a capacity in the initial StringBuilder to accommodate all
- * constants plus this factor times the number of arguments.
- */
- static final int ARGUMENT_SIZE_FACTOR = 4;
+ static final ClassDesc CD_CONCAT = ConstantUtils.binaryNameToDesc(CLASS_NAME);
+ static final ClassDesc CD_StringConcatHelper = ClassDesc.ofDescriptor("Ljava/lang/StringConcatHelper;");
+ static final ClassDesc CD_StringConcatBase = ClassDesc.ofDescriptor("Ljava/lang/StringConcatHelper$StringConcatBase;");
+ static final ClassDesc CD_Array_byte = ClassDesc.ofDescriptor("[B");
+ static final ClassDesc CD_Array_String = ClassDesc.ofDescriptor("[Ljava/lang/String;");
- static final Set SET_OF_STRONG = Set.of(STRONG);
+ static final MethodTypeDesc MTD_byte_char = MethodTypeDesc.of(CD_byte, CD_char);
+ static final MethodTypeDesc MTD_byte = MethodTypeDesc.of(CD_byte);
+ static final MethodTypeDesc MTD_int = MethodTypeDesc.of(CD_int);
+ static final MethodTypeDesc MTD_int_int_boolean = MethodTypeDesc.of(CD_int, CD_int, CD_boolean);
+ static final MethodTypeDesc MTD_int_int_char = MethodTypeDesc.of(CD_int, CD_int, CD_char);
+ static final MethodTypeDesc MTD_int_int_int = MethodTypeDesc.of(CD_int, CD_int, CD_int);
+ static final MethodTypeDesc MTD_int_int_long = MethodTypeDesc.of(CD_int, CD_int, CD_long);
+ static final MethodTypeDesc MTD_int_int_String = MethodTypeDesc.of(CD_int, CD_int, CD_String);
+ static final MethodTypeDesc MTD_String_float = MethodTypeDesc.of(CD_String, CD_float);
+ static final MethodTypeDesc MTD_String_double = MethodTypeDesc.of(CD_String, CD_double);
+ static final MethodTypeDesc MTD_String_Object = MethodTypeDesc.of(CD_String, CD_Object);
- private SimpleStringBuilderStrategy() {
+ static final MethodTypeDesc MTD_INIT = MethodTypeDesc.of(CD_void, CD_Array_String);
+ static final MethodTypeDesc MTD_NEW_ARRAY_SUFFIX = MethodTypeDesc.of(CD_Array_byte, CD_String, CD_int, CD_byte);
+ static final MethodTypeDesc MTD_STRING_INIT = MethodTypeDesc.of(CD_void, CD_Array_byte, CD_byte);
+
+ static final MethodTypeDesc PREPEND_int = MethodTypeDesc.of(CD_int, CD_int, CD_byte, CD_Array_byte, CD_int, CD_String);
+ static final MethodTypeDesc PREPEND_long = MethodTypeDesc.of(CD_int, CD_int, CD_byte, CD_Array_byte, CD_long, CD_String);
+ static final MethodTypeDesc PREPEND_boolean = MethodTypeDesc.of(CD_int, CD_int, CD_byte, CD_Array_byte, CD_boolean, CD_String);
+ static final MethodTypeDesc PREPEND_char = MethodTypeDesc.of(CD_int, CD_int, CD_byte, CD_Array_byte, CD_char, CD_String);
+ static final MethodTypeDesc PREPEND_String = MethodTypeDesc.of(CD_int, CD_int, CD_byte, CD_Array_byte, CD_String, CD_String);
+
+ static final RuntimeVisibleAnnotationsAttribute FORCE_INLINE = RuntimeVisibleAnnotationsAttribute.of(Annotation.of(ClassDesc.ofDescriptor("Ljdk/internal/vm/annotation/ForceInline;")));
+
+ static final MethodType CONSTRUCTOR_METHOD_TYPE = MethodType.methodType(void.class, String[].class);
+ static final Consumer CONSTRUCTOR_BUILDER = new Consumer() {
+ @Override
+ public void accept(CodeBuilder cb) {
+ /*
+ * super(constants);
+ */
+ int thisSlot = cb.receiverSlot(),
+ constantsSlot = cb.parameterSlot(0);
+ cb.aload(thisSlot)
+ .aload(constantsSlot)
+ .invokespecial(CD_StringConcatBase, INIT_NAME, MTD_INIT, false)
+ .return_();
+ }
+ };
+
+ static final ReferencedKeyMap> CACHE =
+ ReferencedKeyMap.create(true, true,
+ new Supplier<>() {
+ @Override
+ public Map, SoftReference> get() {
+ return new ConcurrentHashMap<>(64);
+ }
+ });
+
+ private InlineHiddenClassStrategy() {
// no instantiation
}
- private static MethodHandle generate(Lookup lookup, MethodType args, String[] constants) throws Exception {
- String className = getClassName(lookup.lookupClass());
+ private record MethodHandlePair(MethodHandle constructor, MethodHandle concatenator) { };
- byte[] classBytes = ClassFile.of().build(ConstantUtils.binaryNameToDesc(className),
+ /**
+ * The parameter types are normalized into 7 types: int,long,boolean,char,float,double,Object
+ */
+ private static MethodType erasedArgs(MethodType args) {
+ int parameterCount = args.parameterCount();
+ var paramTypes = new Class>[parameterCount];
+ boolean changed = false;
+ for (int i = 0; i < parameterCount; i++) {
+ Class> cl = args.parameterType(i);
+ // Use int as the logical type for subword integral types
+ // (byte and short). char and boolean require special
+ // handling so don't change the logical type of those
+ if (cl == byte.class || cl == short.class) {
+ cl = int.class;
+ changed = true;
+ } else if (cl != Object.class && !cl.isPrimitive()) {
+ cl = Object.class;
+ changed = true;
+ }
+ paramTypes[i] = cl;
+ }
+ return changed ? MethodType.methodType(args.returnType(), paramTypes) : args;
+ }
+
+ /**
+ * Construct the MethodType of the prepend method, The parameters only support 5 types:
+ * int/long/char/boolean/String. Not int/long/char/boolean type, use String type
+ *
+ * The following is an example of the generated target code:
+ *
+ * int prepend(int length, byte coder, byte[] buff, String[] constants
+ * int arg0, long arg1, boolean arg2, char arg3, String arg5)
+ *
+ */
+ private static MethodTypeDesc prependArgs(MethodType concatArgs) {
+ int parameterCount = concatArgs.parameterCount();
+ var paramTypes = new ClassDesc[parameterCount + 4];
+ paramTypes[0] = CD_int; // length
+ paramTypes[1] = CD_byte; // coder
+ paramTypes[2] = CD_Array_byte; // buff
+ paramTypes[3] = CD_Array_String; // constants
+
+ for (int i = 0; i < parameterCount; i++) {
+ var cl = concatArgs.parameterType(i);
+ paramTypes[i + 4] = needStringOf(cl) ? CD_String : ConstantUtils.classDesc(cl);
+ }
+ return MethodTypeDesc.of(CD_int, paramTypes);
+ }
+
+ /**
+ * Construct the MethodType of the coder method,
+ * The first parameter is the initialized coder, Only parameter types that can be UTF16 are added.
+ */
+ private static MethodTypeDesc coderArgs(MethodType concatArgs) {
+ int parameterCount = concatArgs.parameterCount();
+ List paramTypes = new ArrayList<>();
+ paramTypes.add(CD_int); // init coder
+ for (int i = 0; i < parameterCount; i++) {
+ var cl = concatArgs.parameterType(i);
+ if (maybeUTF16(cl)) {
+ paramTypes.add(cl == char.class ? CD_char : CD_String);
+ }
+ }
+ return MethodTypeDesc.of(CD_int, paramTypes);
+ }
+
+ /**
+ * Construct the MethodType of the length method,
+ * The first parameter is the initialized length
+ */
+ private static MethodTypeDesc lengthArgs(MethodType concatArgs) {
+ int parameterCount = concatArgs.parameterCount();
+ var paramTypes = new ClassDesc[parameterCount + 1];
+ paramTypes[0] = CD_int; // init long
+ for (int i = 0; i < parameterCount; i++) {
+ var cl = concatArgs.parameterType(i);
+ paramTypes[i + 1] = needStringOf(cl) ? CD_String : ConstantUtils.classDesc(cl);
+ }
+ return MethodTypeDesc.of(CD_int, paramTypes);
+ }
+
+ private static MethodHandle generate(Lookup lookup, MethodType args, String[] constants) throws Exception {
+ lookup = STR_LOOKUP;
+ final MethodType concatArgs = erasedArgs(args);
+
+ // 1 argment use built-in method
+ if (args.parameterCount() == 1) {
+ Object concat1 = JLA.stringConcat1(constants);
+ var handle = lookup.findVirtual(concat1.getClass(), METHOD_NAME, concatArgs);
+ return handle.bindTo(concat1);
+ }
+
+ var weakConstructorHandle = CACHE.get(concatArgs);
+ if (weakConstructorHandle != null) {
+ MethodHandlePair handlePair = weakConstructorHandle.get();
+ if (handlePair != null) {
+ try {
+ var instance = handlePair.constructor.invoke(constants);
+ return handlePair.concatenator.bindTo(instance);
+ } catch (Throwable e) {
+ throw new StringConcatException("Exception while utilizing the hidden class", e);
+ }
+ }
+ }
+ MethodTypeDesc lengthArgs = lengthArgs(concatArgs),
+ coderArgs = parameterMaybeUTF16(concatArgs) ? coderArgs(concatArgs) : null,
+ prependArgs = prependArgs(concatArgs);
+
+ byte[] classBytes = ClassFile.of().build(CD_CONCAT,
new Consumer() {
+ final boolean forceInline = concatArgs.parameterCount() < FORCE_INLINE_THRESHOLD;
+
@Override
public void accept(ClassBuilder clb) {
- clb.withFlags(AccessFlag.FINAL, AccessFlag.SUPER, AccessFlag.SYNTHETIC)
- .withMethodBody(METHOD_NAME,
- ConstantUtils.methodTypeDesc(args),
- ClassFile.ACC_FINAL | ClassFile.ACC_PRIVATE | ClassFile.ACC_STATIC,
- generateMethod(constants, args));
+ clb.withSuperclass(CD_StringConcatBase)
+ .withFlags(ACC_FINAL | ACC_SUPER | ACC_SYNTHETIC)
+ .withMethodBody(INIT_NAME, MTD_INIT, 0, CONSTRUCTOR_BUILDER)
+ .withMethod("length",
+ lengthArgs,
+ ACC_STATIC | ACC_PRIVATE,
+ new Consumer() {
+ public void accept(MethodBuilder mb) {
+ if (forceInline) {
+ mb.with(FORCE_INLINE);
+ }
+ mb.withCode(generateLengthMethod(lengthArgs));
+ }
+ })
+ .withMethod("prepend",
+ prependArgs,
+ ACC_STATIC | ACC_PRIVATE,
+ new Consumer() {
+ public void accept(MethodBuilder mb) {
+ if (forceInline) {
+ mb.with(FORCE_INLINE);
+ }
+ mb.withCode(generatePrependMethod(prependArgs));
+ }
+ })
+ .withMethod(METHOD_NAME,
+ ConstantUtils.methodTypeDesc(concatArgs),
+ ACC_FINAL,
+ new Consumer() {
+ public void accept(MethodBuilder mb) {
+ if (forceInline) {
+ mb.with(FORCE_INLINE);
+ }
+ mb.withCode(generateConcatMethod(
+ CD_CONCAT,
+ concatArgs,
+ lengthArgs,
+ coderArgs,
+ prependArgs));
+ }
+ });
+
+ if (coderArgs != null) {
+ clb.withMethod("coder",
+ coderArgs,
+ ACC_STATIC | ACC_PRIVATE,
+ new Consumer() {
+ public void accept(MethodBuilder mb) {
+ if (forceInline) {
+ mb.with(FORCE_INLINE);
+ }
+ mb.withCode(generateCoderMethod(coderArgs));
+ }
+ });
+ }
}});
try {
- Lookup hiddenLookup = lookup.makeHiddenClassDefiner(className, classBytes, SET_OF_STRONG, DUMPER)
- .defineClassAsLookup(true);
- Class> innerClass = hiddenLookup.lookupClass();
- return hiddenLookup.findStatic(innerClass, METHOD_NAME, args);
- } catch (Exception e) {
+ var hiddenClass = lookup.makeHiddenClassDefiner(CLASS_NAME, classBytes, Set.of(), DUMPER)
+ .defineClass(true, null);
+ var constructor = lookup.findConstructor(hiddenClass, CONSTRUCTOR_METHOD_TYPE);
+ var concat = lookup.findVirtual(hiddenClass, METHOD_NAME, concatArgs);
+ CACHE.put(concatArgs, new SoftReference<>(new MethodHandlePair(constructor, concat)));
+ var instance = hiddenClass.cast(constructor.invoke(constants));
+ return concat.bindTo(instance);
+ } catch (Throwable e) {
throw new StringConcatException("Exception while spinning the class", e);
}
}
- private static Consumer generateMethod(String[] constants, MethodType args) {
+ /**
+ * Generate InlineCopy-based code.
+ *
+ * The following is an example of the generated target code:
+ *
+ *
+ * import static java.lang.StringConcatHelper.newArrayWithSuffix;
+ * import static java.lang.StringConcatHelper.prepend;
+ * import static java.lang.StringConcatHelper.stringCoder;
+ * import static java.lang.StringConcatHelper.stringSize;
+ *
+ * class StringConcat extends java.lang.StringConcatHelper.StringConcatBase {
+ * // super class defines
+ * // String[] constants;
+ * // int length;
+ * // byte coder;
+ *
+ * StringConcat(String[] constants) {
+ * super(constants);
+ * }
+ *
+ * String concat(int arg0, long arg1, boolean arg2, char arg3, String arg4,
+ * float arg5, double arg6, Object arg7
+ * ) {
+ * // Types other than byte/short/int/long/boolean/String require a local variable to store
+ * String str4 = stringOf(arg4);
+ * String str5 = stringOf(arg5);
+ * String str6 = stringOf(arg6);
+ * String str7 = stringOf(arg7);
+ *
+ * int coder = coder(this.coder, arg0, arg1, arg2, arg3, str4, str5, str6, str7);
+ * int length = length(this.length, arg0, arg1, arg2, arg3, arg4, arg5, arg6, arg7);
+ * String[] constants = this.constants;
+ * byte[] buf = newArrayWithSuffix(constants[paramCount], length. coder);
+ *
+ * prepend(length, coder, buf, constants, arg0, arg1, arg2, arg3, str4, str5, str6, str7);
+ *
+ * return new String(buf, coder);
+ * }
+ *
+ * static int length(int length, int arg0, long arg1, boolean arg2, char arg3,
+ * String arg4, String arg5, String arg6, String arg7) {
+ * return stringSize(stringSize(stringSize(stringSize(stringSize(stringSize(stringSize(stringSize(
+ * length, arg0), arg1), arg2), arg3), arg4), arg5), arg6), arg7);
+ * }
+ *
+ * static int cocder(int coder, char arg3, String str4, String str5, String str6, String str7) {
+ * return coder | stringCoder(arg3) | str4.coder() | str5.coder() | str6.coder() | str7.coder();
+ * }
+ *
+ * static int prepend(int length, int coder, byte[] buf, String[] constants,
+ * int arg0, long arg1, boolean arg2, char arg3,
+ * String str4, String str5, String str6, String str7) {
+ * // StringConcatHelper.prepend
+ * return prepend(prepend(prepend(prepend(
+ * prepend(apppend(prepend(prepend(length,
+ * buf, str7, constant[7]), buf, str6, constant[6]),
+ * buf, str5, constant[5]), buf, str4, constant[4]),
+ * buf, arg3, constant[3]), buf, arg2, constant[2]),
+ * buf, arg1, constant[1]), buf, arg0, constant[0]);
+ * }
+ * }
+ *
+ */
+ private static Consumer generateConcatMethod(
+ ClassDesc concatClass,
+ MethodType concatArgs,
+ MethodTypeDesc lengthArgs,
+ MethodTypeDesc coderArgs,
+ MethodTypeDesc prependArgs
+ ) {
return new Consumer() {
@Override
public void accept(CodeBuilder cb) {
- cb.new_(STRING_BUILDER);
- cb.dup();
+ // Compute parameter variable slots
+ int paramCount = concatArgs.parameterCount(),
+ thisSlot = cb.receiverSlot(),
+ lengthSlot = cb.allocateLocal(TypeKind.IntType),
+ coderSlot = cb.allocateLocal(TypeKind.ByteType),
+ bufSlot = cb.allocateLocal(TypeKind.ReferenceType),
+ constantsSlot = cb.allocateLocal(TypeKind.ReferenceType),
+ suffixSlot = cb.allocateLocal(TypeKind.ReferenceType);
- int len = 0;
- for (String constant : constants) {
- if (constant != null) {
- len += constant.length();
- }
- }
- len += args.parameterCount() * ARGUMENT_SIZE_FACTOR;
- cb.loadConstant(len);
- cb.invokespecial(STRING_BUILDER, "", INT_CONSTRUCTOR_TYPE);
-
- // At this point, we have a blank StringBuilder on stack, fill it in with .append calls.
- {
- int off = 0;
- for (int c = 0; c < args.parameterCount(); c++) {
- if (constants[c] != null) {
- cb.ldc(constants[c]);
- cb.invokevirtual(STRING_BUILDER, "append", APPEND_STRING_TYPE);
+ /*
+ * Types other than int/long/char/boolean require local variables to store the result of stringOf.
+ *
+ * stringSlots stores the slots of parameters relative to local variables
+ *
+ * str0 = stringOf(arg0);
+ * str1 = stringOf(arg1);
+ * ...
+ * strN = toString(argN);
+ */
+ int[] stringSlots = new int[paramCount];
+ for (int i = 0; i < paramCount; i++) {
+ var cl = concatArgs.parameterType(i);
+ if (needStringOf(cl)) {
+ MethodTypeDesc methodTypeDesc;
+ if (cl == float.class) {
+ methodTypeDesc = MTD_String_float;
+ } else if (cl == double.class) {
+ methodTypeDesc = MTD_String_double;
+ } else {
+ methodTypeDesc = MTD_String_Object;
}
- Class> cl = args.parameterType(c);
- TypeKind kind = TypeKind.from(cl);
- cb.loadLocal(kind, off);
- off += kind.slotSize();
- MethodTypeDesc desc = getSBAppendDesc(cl);
- cb.invokevirtual(STRING_BUILDER, "append", desc);
- }
- if (constants[constants.length - 1] != null) {
- cb.ldc(constants[constants.length - 1]);
- cb.invokevirtual(STRING_BUILDER, "append", APPEND_STRING_TYPE);
+ stringSlots[i] = cb.allocateLocal(TypeKind.ReferenceType);
+ cb.loadLocal(TypeKind.from(cl), cb.parameterSlot(i))
+ .invokestatic(CD_StringConcatHelper, "stringOf", methodTypeDesc)
+ .astore(stringSlots[i]);
}
}
- cb.invokevirtual(STRING_BUILDER, "toString", TO_STRING_TYPE);
- cb.areturn();
+ /*
+ * coder = coder(this.coder, arg0, arg1, ... argN);
+ */
+ cb.aload(thisSlot)
+ .getfield(concatClass, "coder", CD_byte);
+ if (coderArgs != null) {
+ for (int i = 0; i < paramCount; i++) {
+ var cl = concatArgs.parameterType(i);
+ if (maybeUTF16(cl)) {
+ if (cl == char.class) {
+ cb.loadLocal(TypeKind.CharType, cb.parameterSlot(i));
+ } else {
+ cb.aload(stringSlots[i]);
+ }
+ }
+ }
+ cb.invokestatic(concatClass, "coder", coderArgs);
+ }
+ cb.istore(coderSlot);
+
+ /*
+ * length = length(this.length, arg0, arg1, ..., argN);
+ */
+ cb.aload(thisSlot)
+ .getfield(concatClass, "length", CD_int);
+ for (int i = 0; i < paramCount; i++) {
+ var cl = concatArgs.parameterType(i);
+ int paramSlot = cb.parameterSlot(i);
+ if (needStringOf(cl)) {
+ paramSlot = stringSlots[i];
+ cl = String.class;
+ }
+ cb.loadLocal(TypeKind.from(cl), paramSlot);
+ }
+ cb.invokestatic(concatClass, "length", lengthArgs);
+
+ /*
+ * String[] constants = this.constants;
+ * suffix = constants[paranCount];
+ * length -= suffix.length();
+ */
+ cb.aload(thisSlot)
+ .getfield(concatClass, "constants", CD_Array_String)
+ .dup()
+ .astore(constantsSlot)
+ .ldc(paramCount)
+ .aaload()
+ .dup()
+ .astore(suffixSlot)
+ .invokevirtual(CD_String, "length", MTD_int)
+ .isub()
+ .istore(lengthSlot);
+
+ /*
+ * Allocate buffer :
+ *
+ * buf = newArrayWithSuffix(suffix, length, coder)
+ */
+ cb.aload(suffixSlot)
+ .iload(lengthSlot)
+ .iload(coderSlot)
+ .invokestatic(CD_StringConcatHelper, "newArrayWithSuffix", MTD_NEW_ARRAY_SUFFIX)
+ .astore(bufSlot);
+
+ /*
+ * prepend(length, coder, buf, constants, ar0, ar1, ..., argN);
+ */
+ cb.iload(lengthSlot)
+ .iload(coderSlot)
+ .aload(bufSlot)
+ .aload(constantsSlot);
+ for (int i = 0; i < paramCount; i++) {
+ var cl = concatArgs.parameterType(i);
+ int paramSlot = cb.parameterSlot(i);
+ var kind = TypeKind.from(cl);
+ if (needStringOf(cl)) {
+ paramSlot = stringSlots[i];
+ kind = TypeKind.ReferenceType;
+ }
+ cb.loadLocal(kind, paramSlot);
+ }
+ cb.invokestatic(concatClass, "prepend", prependArgs);
+
+ // return new String(buf, coder);
+ cb.new_(CD_String)
+ .dup()
+ .aload(bufSlot)
+ .iload(coderSlot)
+ .invokespecial(CD_String, INIT_NAME, MTD_STRING_INIT)
+ .areturn();
}
};
}
/**
- * The generated class is in the same package as the host class as
- * it's the implementation of the string concatenation for the host
- * class.
+ * Generate length method.
+ *
+ * The following is an example of the generated target code:
+ *
+ *
+ * import static java.lang.StringConcatHelper.stringSize;
+ *
+ * static int length(int length, int arg0, long arg1, boolean arg2, char arg3,
+ * String arg4, String arg5, String arg6, String arg7) {
+ * return stringSize(stringSize(stringSize(length, arg0), arg1), ..., arg7);
+ * }
+ *
*/
- private static String getClassName(Class> hostClass) {
- String name = hostClass.isHidden() ? hostClass.getName().replace('/', '_')
- : hostClass.getName();
- return name + "$$StringConcat";
+ private static Consumer generateLengthMethod(MethodTypeDesc lengthArgs) {
+ return new Consumer() {
+ @Override
+ public void accept(CodeBuilder cb) {
+ int lengthSlot = cb.parameterSlot(0);
+ cb.iload(lengthSlot);
+ for (int i = 1; i < lengthArgs.parameterCount(); i++) {
+ var cl = lengthArgs.parameterType(i);
+ MethodTypeDesc methodTypeDesc;
+ if (cl == CD_char) {
+ methodTypeDesc = MTD_int_int_char;
+ } else if (cl == CD_int) {
+ methodTypeDesc = MTD_int_int_int;
+ } else if (cl == CD_long) {
+ methodTypeDesc = MTD_int_int_long;
+ } else if (cl == CD_boolean) {
+ methodTypeDesc = MTD_int_int_boolean;
+ } else {
+ methodTypeDesc = MTD_int_int_String;
+ }
+ cb.loadLocal(TypeKind.from(cl), cb.parameterSlot(i))
+ .invokestatic(CD_StringConcatHelper, "stringSize", methodTypeDesc);
+ }
+ cb.ireturn();
+ }
+ };
}
- private static MethodTypeDesc getSBAppendDesc(Class> cl) {
- if (cl.isPrimitive()) {
- if (cl == Integer.TYPE || cl == Byte.TYPE || cl == Short.TYPE) {
- return APPEND_INT_TYPE;
- } else if (cl == Boolean.TYPE) {
- return APPEND_BOOLEAN_TYPE;
- } else if (cl == Character.TYPE) {
- return APPEND_CHAR_TYPE;
- } else if (cl == Double.TYPE) {
- return APPEND_DOUBLE_TYPE;
- } else if (cl == Float.TYPE) {
- return APPEND_FLOAT_TYPE;
- } else if (cl == Long.TYPE) {
- return APPEND_LONG_TYPE;
- } else {
- throw new IllegalStateException("Unhandled primitive StringBuilder.append: " + cl);
+ /**
+ * Generate coder method.
+ *
+ * The following is an example of the generated target code:
+ *
+ *
+ * import static java.lang.StringConcatHelper.stringCoder;
+ *
+ * static int cocder(int coder, char arg3, String str4, String str5, String str6, String str7) {
+ * return coder | stringCoder(arg3) | str4.coder() | str5.coder() | str6.coder() | str7.coder();
+ * }
+ *
+ */
+ private static Consumer generateCoderMethod(MethodTypeDesc coderArgs) {
+ return new Consumer() {
+ @Override
+ public void accept(CodeBuilder cb) {
+ /*
+ * return coder | stringCoder(argN) | ... | arg1.coder() | arg0.coder();
+ */
+ int coderSlot = cb.parameterSlot(0);
+ cb.iload(coderSlot);
+ for (int i = 1; i < coderArgs.parameterCount(); i++) {
+ var cl = coderArgs.parameterType(i);
+ cb.loadLocal(TypeKind.from(cl), cb.parameterSlot(i));
+ if (cl == CD_char) {
+ cb.invokestatic(CD_StringConcatHelper, "stringCoder", MTD_byte_char);
+ } else {
+ cb.invokevirtual(CD_String, "coder", MTD_byte);
+ }
+ cb.ior();
+ }
+ cb.ireturn();
+ }
+ };
+ }
+
+ /**
+ * Generate prepend method.
+ *
+ * The following is an example of the generated target code:
+ *
+ *
+ * import static java.lang.StringConcatHelper.prepend;
+ *
+ * static int prepend(int length, int coder, byte[] buf, String[] constants,
+ * int arg0, long arg1, boolean arg2, char arg3,
+ * String str4, String str5, String str6, String str7) {
+ *
+ * return prepend(prepend(prepend(prepend(
+ * prepend(prepend(prepend(prepend(length,
+ * buf, str7, constant[7]), buf, str6, constant[6]),
+ * buf, str5, constant[5]), buf, str4, constant[4]),
+ * buf, arg3, constant[3]), buf, arg2, constant[2]),
+ * buf, arg1, constant[1]), buf, arg0, constant[0]);
+ * }
+ *
+ */
+ private static Consumer generatePrependMethod(MethodTypeDesc prependArgs) {
+ return new Consumer() {
+ @Override
+ public void accept(CodeBuilder cb) {
+ // Compute parameter variable slots
+ int lengthSlot = cb.parameterSlot(0),
+ coderSlot = cb.parameterSlot(1),
+ bufSlot = cb.parameterSlot(2),
+ constantsSlot = cb.parameterSlot(3);
+ /*
+ * // StringConcatHelper.prepend
+ * return prepend(prepend(prepend(prepend(
+ * prepend(apppend(prepend(prepend(length,
+ * buf, str7, constant[7]), buf, str6, constant[6]),
+ * buf, str5, constant[5]), buf, arg4, constant[4]),
+ * buf, arg3, constant[3]), buf, arg2, constant[2]),
+ * buf, arg1, constant[1]), buf, arg0, constant[0]);
+ */
+ cb.iload(lengthSlot);
+ for (int i = prependArgs.parameterCount() - 1; i >= 4; i--) {
+ var cl = prependArgs.parameterType(i);
+ var kind = TypeKind.from(cl);
+
+ // There are only 5 types of parameters: int, long, boolean, char, String
+ MethodTypeDesc methodTypeDesc;
+ if (cl == CD_int) {
+ methodTypeDesc = PREPEND_int;
+ } else if (cl == CD_long) {
+ methodTypeDesc = PREPEND_long;
+ } else if (cl == CD_boolean) {
+ methodTypeDesc = PREPEND_boolean;
+ } else if (cl == CD_char) {
+ methodTypeDesc = PREPEND_char;
+ } else {
+ kind = TypeKind.ReferenceType;
+ methodTypeDesc = PREPEND_String;
+ }
+
+ cb.iload(coderSlot)
+ .aload(bufSlot)
+ .loadLocal(kind, cb.parameterSlot(i))
+ .aload(constantsSlot)
+ .ldc(i - 4)
+ .aaload()
+ .invokestatic(CD_StringConcatHelper, "prepend", methodTypeDesc);
+ }
+ cb.ireturn();
+ }
+ };
+ }
+
+ static boolean needStringOf(Class> cl) {
+ return cl != int.class && cl != long.class && cl != boolean.class && cl != char.class;
+ }
+
+ static boolean maybeUTF16(Class> cl) {
+ return cl == char.class || !cl.isPrimitive();
+ }
+
+ static boolean parameterMaybeUTF16(MethodType args) {
+ for (int i = 0; i < args.parameterCount(); i++) {
+ if (maybeUTF16(args.parameterType(i))) {
+ return true;
}
- } else if (cl == String.class) {
- return APPEND_STRING_TYPE;
- } else {
- return APPEND_OBJECT_TYPE;
}
+ return false;
}
}
}
diff --git a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
index 24aeabf6d36..e4d322a20d7 100644
--- a/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
+++ b/src/java.base/share/classes/jdk/internal/access/JavaLangAccess.java
@@ -446,6 +446,8 @@ public interface JavaLangAccess {
*/
long stringConcatMix(long lengthCoder, char value);
+ Object stringConcat1(String[] constants);
+
/**
* Join strings
*/
diff --git a/src/java.base/share/classes/jdk/internal/util/ClassFileDumper.java b/src/java.base/share/classes/jdk/internal/util/ClassFileDumper.java
index afb3d1374ab..b104b56ac0e 100644
--- a/src/java.base/share/classes/jdk/internal/util/ClassFileDumper.java
+++ b/src/java.base/share/classes/jdk/internal/util/ClassFileDumper.java
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2013, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2013, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -25,7 +25,6 @@
package jdk.internal.util;
import jdk.internal.misc.VM;
-import sun.security.action.GetPropertyAction;
import java.io.IOException;
import java.nio.file.Files;
@@ -80,7 +79,11 @@ public final class ClassFileDumper {
private final AtomicInteger counter = new AtomicInteger();
private ClassFileDumper(String key, String path) {
- String value = GetPropertyAction.privilegedGetProperty(key);
+ /*
+ * GetPropertyAction.privilegedGetProperty cannot be used here, Using VM.getSavedProperty to avoid a bootstrap
+ * circularity issue in the java/lang/String/concat/WithSecurityManager.java test
+ */
+ String value = VM.getSavedProperty(key);
this.key = key;
boolean enabled = value != null && value.isEmpty() ? true : Boolean.parseBoolean(value);
if (enabled) {
diff --git a/test/jdk/java/lang/String/concat/HiddenClassUnloading.java b/test/jdk/java/lang/String/concat/HiddenClassUnloading.java
new file mode 100644
index 00000000000..9e682f38ef5
--- /dev/null
+++ b/test/jdk/java/lang/String/concat/HiddenClassUnloading.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+import java.lang.StringBuilder;
+
+import java.lang.invoke.*;
+import java.lang.management.ManagementFactory;
+
+/**
+ * @test
+ * @summary Test whether the hidden class unloading of StringConcatFactory works
+ *
+ * @requires vm.flagless
+ * @run main/othervm -Xmx8M -Xms8M -Xverify:all HiddenClassUnloading
+ * @run main/othervm -Xmx8M -Xms8M -Xverify:all -XX:-CompactStrings HiddenClassUnloading
+ */
+public class HiddenClassUnloading {
+ public static void main(String[] args) throws Throwable {
+ var lookup = MethodHandles.lookup();
+ var types = new Class>[] {
+ int.class, long.class, double.class, float.class, char.class, boolean.class, String.class,
+ };
+
+ long initUnloadedClassCount = ManagementFactory.getClassLoadingMXBean().getUnloadedClassCount();
+
+ for (int i = 0; i < 2000; i++) {
+ int radix = types.length;
+ String str = Integer.toString(i, radix);
+ int length = str.length();
+ var ptypes = new Class[length];
+ for (int j = 0; j < length; j++) {
+ int index = Integer.parseInt(str.substring(j, j + 1), radix);
+ ptypes[j] = types[index];
+ }
+ StringConcatFactory.makeConcatWithConstants(
+ lookup,
+ "concat",
+ MethodType.methodType(String.class, ptypes),
+ "\1".repeat(length), // recipe
+ new Object[0]
+ );
+ }
+
+ long unloadedClassCount = ManagementFactory.getClassLoadingMXBean().getUnloadedClassCount();
+ if (initUnloadedClassCount == unloadedClassCount) {
+ throw new RuntimeException("unloadedClassCount is zero");
+ }
+ }
+}
diff --git a/test/micro/org/openjdk/bench/java/lang/StringConcat.java b/test/micro/org/openjdk/bench/java/lang/StringConcat.java
index 015ad224631..e1b8d882dd9 100644
--- a/test/micro/org/openjdk/bench/java/lang/StringConcat.java
+++ b/test/micro/org/openjdk/bench/java/lang/StringConcat.java
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -48,22 +49,40 @@ public class StringConcat {
@Param("4711")
public int intValue;
-
+ public Integer integerValue = intValue;
+ public float floatValue = 156456.36435637F + intValue;
public String stringValue = String.valueOf(intValue);
-
public Object objectValue = Long.valueOf(intValue);
-
public boolean boolValue = true;
-
+ public Boolean booleanValue = Boolean.TRUE;
public byte byteValue = (byte)-128;
-
public String emptyString = "";
+ @Benchmark
+ public String concatConstBool() {
+ return "string" + boolValue;
+ }
+
+ @Benchmark
+ public String concatConstBoolean() {
+ return "string" + booleanValue;
+ }
+
@Benchmark
public String concatConstInt() {
return "string" + intValue;
}
+ @Benchmark
+ public String concatConstInteger() {
+ return "string" + integerValue;
+ }
+
+ @Benchmark
+ public String concatConstFloat() {
+ return "string" + floatValue;
+ }
+
@Benchmark
public String concatConstString() {
return "string" + stringValue;
@@ -94,6 +113,31 @@ public class StringConcat {
return "string".concat(stringValue);
}
+ @Benchmark
+ public String concatConstBoolString() {
+ return "string" + boolValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatConstBooleanString() {
+ return "string" + booleanValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatConstIntString() {
+ return "string" + intValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatConstIntegerString() {
+ return "string" + integerValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatConstFloatString() {
+ return "string" + floatValue + stringValue;
+ }
+
@Benchmark
public String concatConstIntConstInt() {
return "string" + intValue + "string" + intValue;
@@ -104,6 +148,36 @@ public class StringConcat {
return "string" + stringValue + "string" + intValue;
}
+ @Benchmark
+ public String concatConstStringConst() {
+ return "string" + stringValue + "string";
+ }
+
+ @Benchmark
+ public String concatConstIntConst() {
+ return "string" + intValue + "string";
+ }
+
+ @Benchmark
+ public String concatConstIntegerConst() {
+ return "string" + integerValue + "string";
+ }
+
+ @Benchmark
+ public String concatConstFloatConst() {
+ return "string" + floatValue + "string";
+ }
+
+ @Benchmark
+ public String concatConstObjectConst() {
+ return "string" + objectValue + "string";
+ }
+
+ @Benchmark
+ public String concatConstBooleanConst() {
+ return "string" + booleanValue + "string";
+ }
+
@Benchmark
public String concatMix4String() {
// Investigate "profile pollution" between shared LFs that might eliminate some JIT optimizations
@@ -114,6 +188,31 @@ public class StringConcat {
return s1 + s2 + s3 + s4;
}
+ @Benchmark
+ public String concat3String() {
+ return stringValue + stringValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatStringBoolString() {
+ return stringValue + boolValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatStringBooleanString() {
+ return stringValue + booleanValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatStringIntString() {
+ return stringValue + intValue + stringValue;
+ }
+
+ @Benchmark
+ public String concatStringIntegerString() {
+ return stringValue + integerValue + stringValue;
+ }
+
@Benchmark
public String concatConst4String() {
return "string" + stringValue + stringValue + stringValue + stringValue;
@@ -176,6 +275,15 @@ public class StringConcat {
+ f10 + ","+ f11 + ","+ f12 + ","+ f13 + ","+ f14 + ","+ f15 + ","+ f16 + ","+ f17 + ","+ f18 + ","+ f19 + ","
+ f20 + ","+ f21 + ","+ f22;
}
+
+ @Benchmark
+ public String concat30Mix() {
+ return f0 + "," + f1 + ","+ f2 + ","+ f3 + ","+ f4 + ","+ f5 + ","+ f6 + ","+ f7 + ","+ f8 + ","+ f9 + ","
+ +f10 + ","+f11 + ","+f12 + ","+ f13 + ","+ f14 + ","+ f15 + ","+ f16 + ","+ f17 + ","+ f18 + ","+ f19 + ","
+ +f20 + ","+f21 + ","+f22 + "," + boolValue + "," + booleanValue + "," + intValue + "," + integerValue
+ + "," + floatValue + "," + byteValue + "," + objectValue;
+ }
+
@Benchmark
public String concat123String() {
return f0 + ","+ f1 + ","+ f2 + ","+ f3 + ","+ f4 + ","+ f5 + ","+ f6 + ","+ f7 + ","+ f8 + ","+ f9 + ","
@@ -193,9 +301,38 @@ public class StringConcat {
+f120 + ","+f121 + ","+f122;
}
+ @Benchmark
+ public String concat13StringConst() {
+ return f0 + f1 + f2 + f3 + f4
+ + f5 + f6 + f7 + f8 + f9
+ +f10 + f11 + f12 + """
+ A really long constant string. Such as a copyright header:
+ * Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ """;
+ }
+
@Benchmark
public String concat23StringConst() {
- return f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + f0 + """
+ return f0 + f1 + f2 + f3 + f4 + f5 + f6 + f7 + f8 + f9 + f10 + f11 + f12 + f13 + f14 + f15 + f16 + f17 + f18 + f19 + f20 + f21 + f22 + """
A really long constant string. Such as a copyright header:
* Copyright (c) 2018, 2024, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
diff --git a/test/micro/org/openjdk/bench/java/lang/StringConcatStartup.java b/test/micro/org/openjdk/bench/java/lang/StringConcatStartup.java
index d146fbf9885..cb3d09f94ad 100644
--- a/test/micro/org/openjdk/bench/java/lang/StringConcatStartup.java
+++ b/test/micro/org/openjdk/bench/java/lang/StringConcatStartup.java
@@ -1,5 +1,6 @@
/*
* Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2024, Alibaba Group Holding Limited. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
@@ -27,6 +28,7 @@ import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
+import org.openjdk.jmh.annotations.Param;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.State;
@@ -44,13 +46,31 @@ import java.util.concurrent.TimeUnit;
public class StringConcatStartup {
public static void main(String... args) {
- String[] selection = new String[] { "StringLarge", "MixedSmall", "StringSingle", "MixedLarge" };
+ String[] selection = {
+ "StringLarge",
+ "MixedSmall",
+ "StringSingle",
+ "StringThree",
+ "MixedLarge"
+ };
if (args.length > 0) {
selection = args;
}
for (String select : selection) {
switch (select) {
- case "StringSingle" -> new StringSingle().run();
+ case "StringSingle" -> {
+ new StringSingle().constInt();
+ new StringSingle().constFloat();
+ new StringSingle().constString();
+ new StringSingle().const2String();
+ new StringSingle().constIntString();
+ new StringSingle().constFloatString();
+ new StringSingle().constBooleanString();
+ }
+ case "StringThree" -> {
+ new StringThree().stringIntString();
+ new StringThree().stringIntegerString();
+ }
case "MixedSmall" -> new MixedSmall().run();
case "StringLarge" -> new StringLarge().run();
case "MixedLarge" -> new MixedLarge().run();
@@ -64,14 +84,95 @@ public class StringConcatStartup {
@Fork(value = 40, warmups = 2)
public static class StringSingle {
- public String s = "foo";
+ @Param("4711")
+ public int intValue;
+ public Integer integerValue = intValue;
+ public float floatValue = 156456.36435637F + intValue;
+ public String stringValue = String.valueOf(intValue);
+ public boolean boolValue = true;
+ public Boolean booleanValue = Boolean.TRUE;
@Benchmark
- public String run() {
- return "" + s;
+ public String constBool() {
+ return "string" + boolValue;
+ }
+
+ @Benchmark
+ public String constBoolean() {
+ return "string" + booleanValue;
+ }
+
+ @Benchmark
+ public String constInt() {
+ return "string" + intValue;
+ }
+
+ @Benchmark
+ public String constInteger() {
+ return "string" + integerValue;
+ }
+
+ @Benchmark
+ public String constFloat() {
+ return "string" + floatValue;
+ }
+
+ @Benchmark
+ public String constString() {
+ return "string" + stringValue;
+ }
+
+ public String const2String() {
+ return "string" + stringValue + stringValue;
+ }
+
+ @Benchmark
+ public String constIntString() {
+ return "string" + intValue + stringValue;
+ }
+
+ @Benchmark
+ public String constIntegerString() {
+ return "string" + integerValue + stringValue;
+ }
+
+ @Benchmark
+ public String constFloatString() {
+ return "string" + floatValue + stringValue;
+ }
+
+ @Benchmark
+ public String constBoolString() {
+ return "string" + boolValue + stringValue;
+ }
+
+ @Benchmark
+ public String constBooleanString() {
+ return "string" + booleanValue + stringValue;
}
}
+ @BenchmarkMode(Mode.SingleShotTime)
+ @OutputTimeUnit(TimeUnit.MILLISECONDS)
+ @State(Scope.Thread)
+ @Fork(value = 40, warmups = 2)
+ public static class StringThree {
+
+ @Param("4711")
+ public int intValue;
+ public Integer integerValue = intValue;
+ public String stringValue = String.valueOf(intValue);
+
+ @Benchmark
+ public String stringIntString() {
+ return stringValue + intValue + stringValue;
+ }
+
+ @Benchmark
+ public String stringIntegerString() {
+ return stringValue + integerValue + stringValue;
+ }
+ }
@BenchmarkMode(Mode.SingleShotTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)