From 5022109b2a33a8cf2608eb829098b27641b731a4 Mon Sep 17 00:00:00 2001 From: Shaojin Wen Date: Fri, 16 Aug 2024 13:18:02 +0000 Subject: [PATCH] 8336856: Efficient hidden class-based string concatenation strategy Co-authored-by: Claes Redestad Reviewed-by: redestad, liach --- .../classes/java/lang/StringConcatHelper.java | 343 ++++++++ .../share/classes/java/lang/System.java | 4 + .../java/lang/invoke/StringConcatFactory.java | 788 +++++++++++++++--- .../jdk/internal/access/JavaLangAccess.java | 2 + .../jdk/internal/util/ClassFileDumper.java | 9 +- .../String/concat/HiddenClassUnloading.java | 69 ++ .../openjdk/bench/java/lang/StringConcat.java | 149 +++- .../bench/java/lang/StringConcatStartup.java | 111 ++- 8 files changed, 1329 insertions(+), 146 deletions(-) create mode 100644 test/jdk/java/lang/String/concat/HiddenClassUnloading.java 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)