8336856: Efficient hidden class-based string concatenation strategy

Co-authored-by: Claes Redestad <redestad@openjdk.org>
Reviewed-by: redestad, liach
This commit is contained in:
Shaojin Wen 2024-08-16 13:18:02 +00:00 committed by Claes Redestad
parent ddbc0b6a39
commit 5022109b2a
8 changed files with 1329 additions and 146 deletions

View File

@ -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");
}
}

View File

@ -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);
}

View File

@ -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;
}
/**
* <p>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
*
* <p>This strategy emits StringBuilder chains as similar as possible
* to what javac would. No exact sizing of parameters or estimates.
* <p>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 {
private static final class InlineHiddenClassStrategy {
static final String CLASS_NAME = "java.lang.String$$StringConcat";
static final String METHOD_NAME = "concat";
static final ClassDesc STRING_BUILDER = ClassDesc.ofDescriptor("Ljava/lang/StringBuilder;");
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 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 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);
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<CodeBuilder> CONSTRUCTOR_BUILDER = new Consumer<CodeBuilder>() {
@Override
public void accept(CodeBuilder cb) {
/*
* super(constants);
*/
static final int ARGUMENT_SIZE_FACTOR = 4;
int thisSlot = cb.receiverSlot(),
constantsSlot = cb.parameterSlot(0);
cb.aload(thisSlot)
.aload(constantsSlot)
.invokespecial(CD_StringConcatBase, INIT_NAME, MTD_INIT, false)
.return_();
}
};
static final Set<Lookup.ClassOption> SET_OF_STRONG = Set.of(STRONG);
static final ReferencedKeyMap<MethodType, SoftReference<MethodHandlePair>> CACHE =
ReferencedKeyMap.create(true, true,
new Supplier<>() {
@Override
public Map<ReferenceKey<MethodType>, SoftReference<MethodHandlePair>> get() {
return new ConcurrentHashMap<>(64);
}
});
private SimpleStringBuilderStrategy() {
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<p>
*
* The following is an example of the generated target code:
* <blockquote><pre>
* int prepend(int length, byte coder, byte[] buff, String[] constants
* int arg0, long arg1, boolean arg2, char arg3, String arg5)
* </pre></blockquote>
*/
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<ClassDesc> 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<ClassBuilder>() {
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<MethodBuilder>() {
public void accept(MethodBuilder mb) {
if (forceInline) {
mb.with(FORCE_INLINE);
}
mb.withCode(generateLengthMethod(lengthArgs));
}
})
.withMethod("prepend",
prependArgs,
ACC_STATIC | ACC_PRIVATE,
new Consumer<MethodBuilder>() {
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<MethodBuilder>() {
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<MethodBuilder>() {
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<CodeBuilder> generateMethod(String[] constants, MethodType args) {
/**
* Generate InlineCopy-based code. <p>
*
* The following is an example of the generated target code:
*
* <blockquote><pre>
* 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]);
* }
* }
* </pre></blockquote>
*/
private static Consumer<CodeBuilder> generateConcatMethod(
ClassDesc concatClass,
MethodType concatArgs,
MethodTypeDesc lengthArgs,
MethodTypeDesc coderArgs,
MethodTypeDesc prependArgs
) {
return new Consumer<CodeBuilder>() {
@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();
/*
* 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;
}
}
len += args.parameterCount() * ARGUMENT_SIZE_FACTOR;
cb.loadConstant(len);
cb.invokespecial(STRING_BUILDER, "<init>", 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);
}
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. <p>
*
* The following is an example of the generated target code:
*
* <blockquote><pre>
* 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);
* }
* </pre></blockquote>
*/
private static String getClassName(Class<?> hostClass) {
String name = hostClass.isHidden() ? hostClass.getName().replace('/', '_')
: hostClass.getName();
return name + "$$StringConcat";
private static Consumer<CodeBuilder> generateLengthMethod(MethodTypeDesc lengthArgs) {
return new Consumer<CodeBuilder>() {
@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;
/**
* Generate coder method. <p>
*
* The following is an example of the generated target code:
*
* <blockquote><pre>
* 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();
* }
* </pre></blockquote>
*/
private static Consumer<CodeBuilder> generateCoderMethod(MethodTypeDesc coderArgs) {
return new Consumer<CodeBuilder>() {
@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 {
throw new IllegalStateException("Unhandled primitive StringBuilder.append: " + cl);
cb.invokevirtual(CD_String, "coder", MTD_byte);
}
} else if (cl == String.class) {
return APPEND_STRING_TYPE;
cb.ior();
}
cb.ireturn();
}
};
}
/**
* Generate prepend method. <p>
*
* The following is an example of the generated target code:
*
* <blockquote><pre>
* 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]);
* }
* </pre></blockquote>
*/
private static Consumer<CodeBuilder> generatePrependMethod(MethodTypeDesc prependArgs) {
return new Consumer<CodeBuilder>() {
@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 {
return APPEND_OBJECT_TYPE;
}
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;
}
}
return false;
}
}
}

View File

@ -446,6 +446,8 @@ public interface JavaLangAccess {
*/
long stringConcatMix(long lengthCoder, char value);
Object stringConcat1(String[] constants);
/**
* Join strings
*/

View File

@ -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) {

View File

@ -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");
}
}
}

View File

@ -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.

View File

@ -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)