8222484: Specialize generation of simple String concatenation expressions
Reviewed-by: jrose, jlaskey
--- a/make/jdk/src/classes/build/tools/classlist/HelloClasslist.java Tue Apr 16 21:29:33 2019 +0000
+++ b/make/jdk/src/classes/build/tools/classlist/HelloClasslist.java Wed Apr 17 00:06:38 2019 +0200
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2016, 2019, 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
@@ -67,13 +67,24 @@
.forEach(System.out::println);
// Common concatenation patterns
- String const_I = "string" + args.length;
- String const_S = "string" + String.valueOf(args.length);
- String S_const = String.valueOf(args.length) + "string";
- String S_S = String.valueOf(args.length) + String.valueOf(args.length);
- String const_J = "string" + System.currentTimeMillis();
- String I_const = args.length + "string";
- String J_const = System.currentTimeMillis() + "string";
+ String SS = String.valueOf(args.length) + String.valueOf(args.length);
+ String CS = "string" + String.valueOf(args.length);
+ String SC = String.valueOf(args.length) + "string";
+ String SCS = String.valueOf(args.length) + "string" + String.valueOf(args.length);
+ String CSS = "string" + String.valueOf(args.length) + String.valueOf(args.length);
+ String CSCS = "string" + String.valueOf(args.length) + "string" + String.valueOf(args.length);
+ String SCSC = String.valueOf(args.length) + "string" + String.valueOf(args.length) + "string";
+ String CSCSC = "string" + String.valueOf(args.length) + "string" + String.valueOf(args.length) + "string";
+ String SCSCS = String.valueOf(args.length) + "string" + String.valueOf(args.length) + "string" + String.valueOf(args.length);
+ String CI = "string" + args.length;
+ String IC = args.length + "string";
+ String CIC = "string" + args.length + "string";
+ String CICI = "string" + args.length + "string" + args.length;
+ String CJ = "string" + System.currentTimeMillis();
+ String JC = System.currentTimeMillis() + "string";
+ String CJC = "string" + System.currentTimeMillis() + "string";
+ String CJCJ = "string" + System.currentTimeMillis() + "string" + System.currentTimeMillis();
+ String CJCJC = "string" + System.currentTimeMillis() + "string" + System.currentTimeMillis() + "string";
String newDate = DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(
LocalDateTime.now(ZoneId.of("GMT")));
--- a/src/java.base/share/classes/java/lang/String.java Tue Apr 16 21:29:33 2019 +0000
+++ b/src/java.base/share/classes/java/lang/String.java Wed Apr 17 00:06:38 2019 +0200
@@ -1965,20 +1965,7 @@
if (str.isEmpty()) {
return this;
}
- if (coder() == str.coder()) {
- byte[] val = this.value;
- byte[] oval = str.value;
- int len = val.length + oval.length;
- byte[] buf = Arrays.copyOf(val, len);
- System.arraycopy(oval, 0, buf, val.length, oval.length);
- return new String(buf, coder);
- }
- int len = length();
- int olen = str.length();
- byte[] buf = StringUTF16.newBytesFor(len + olen);
- getBytes(buf, 0, UTF16);
- str.getBytes(buf, len, UTF16);
- return new String(buf, UTF16);
+ return StringConcatHelper.simpleConcat(this, str);
}
/**
--- a/src/java.base/share/classes/java/lang/StringConcatHelper.java Tue Apr 16 21:29:33 2019 +0000
+++ b/src/java.base/share/classes/java/lang/StringConcatHelper.java Wed Apr 17 00:06:38 2019 +0200
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2019, 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,6 +25,9 @@
package java.lang;
+import jdk.internal.misc.Unsafe;
+import jdk.internal.vm.annotation.ForceInline;
+
/**
* Helper for string concatenation. These methods are mostly looked up with private lookups
* from {@link java.lang.invoke.StringConcatFactory}, and used in {@link java.lang.invoke.MethodHandle}
@@ -38,8 +41,10 @@
/**
* Check for overflow, throw exception on overflow.
- * @param lengthCoder String length and coder
- * @return lengthCoder
+ *
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @return the given parameter value, if valid
*/
private static long checkOverflow(long lengthCoder) {
if ((int)lengthCoder >= 0) {
@@ -50,76 +55,83 @@
/**
* Mix value length and coder into current length and coder.
- * @param current current length
- * @param value value to mix in
- * @return new length and coder
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
*/
- static long mix(long current, boolean value) {
- return checkOverflow(current + (value ? 4 : 5));
+ static long mix(long lengthCoder, boolean value) {
+ return checkOverflow(lengthCoder + (value ? 4 : 5));
}
/**
* Mix value length and coder into current length and coder.
- * @param current current length
- * @param value value to mix in
- * @return new length and coder
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
*/
- static long mix(long current, byte value) {
- return mix(current, (int)value);
+ static long mix(long lengthCoder, byte value) {
+ return mix(lengthCoder, (int)value);
}
/**
* Mix value length and coder into current length and coder.
- * @param current current length
- * @param value value to mix in
- * @return new length and coder
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
*/
- static long mix(long current, char value) {
- return checkOverflow(current + 1) | (StringLatin1.canEncode(value) ? 0 : UTF16);
+ static long mix(long lengthCoder, char value) {
+ return checkOverflow(lengthCoder + 1) | (StringLatin1.canEncode(value) ? 0 : UTF16);
}
/**
* Mix value length and coder into current length and coder.
- * @param current current length
- * @param value value to mix in
- * @return new length and coder
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
*/
- static long mix(long current, short value) {
- return mix(current, (int)value);
+ static long mix(long lengthCoder, short value) {
+ return mix(lengthCoder, (int)value);
}
/**
* Mix value length and coder into current length and coder.
- * @param current current length
- * @param value value to mix in
- * @return new length and coder
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
*/
- static long mix(long current, int value) {
- return checkOverflow(current + Integer.stringSize(value));
+ static long mix(long lengthCoder, int value) {
+ return checkOverflow(lengthCoder + Integer.stringSize(value));
}
/**
* Mix value length and coder into current length and coder.
- * @param current current length
- * @param value value to mix in
- * @return new length and coder
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
*/
- static long mix(long current, long value) {
- return checkOverflow(current + Long.stringSize(value));
+ static long mix(long lengthCoder, long value) {
+ return checkOverflow(lengthCoder + Long.stringSize(value));
}
/**
* Mix value length and coder into current length and coder.
- * @param current current length
- * @param value value to mix in
- * @return new length and coder
+ * @param lengthCoder String length with coder packed into higher bits
+ * the upper word.
+ * @param value value to mix in
+ * @return new length and coder
*/
- static long mix(long current, String value) {
- current += value.length();
+ static long mix(long lengthCoder, String value) {
+ lengthCoder += value.length();
if (value.coder() == String.UTF16) {
- current |= UTF16;
+ lengthCoder |= UTF16;
}
- return checkOverflow(current);
+ return checkOverflow(lengthCoder);
}
/**
@@ -285,10 +297,62 @@
}
}
+ /**
+ * Perform a simple concatenation between two objects. Added for startup
+ * performance, but also demonstrates the code that would be emitted by
+ * {@code java.lang.invoke.StringConcatFactory$MethodHandleInlineCopyStrategy}
+ * for two Object arguments.
+ *
+ * @param first first argument
+ * @param second second argument
+ * @return String resulting string
+ */
+ @ForceInline
+ static String simpleConcat(Object first, Object second) {
+ String s1 = stringOf(first);
+ String s2 = stringOf(second);
+ // start "mixing" in length and coder or arguments, order is not
+ // important
+ long indexCoder = mix(initialCoder(), s2);
+ indexCoder = mix(indexCoder, s1);
+ byte[] buf = newArray(indexCoder);
+ // prepend each argument in reverse order, since we prepending
+ // from the end of the byte array
+ indexCoder = prepend(indexCoder, buf, s2);
+ indexCoder = prepend(indexCoder, buf, s1);
+ return newString(buf, indexCoder);
+ }
+
+ /**
+ * We need some additional conversion for Objects in general, because
+ * {@code String.valueOf(Object)} may return null. String conversion rules
+ * in Java state we need to produce "null" String in this case, so we
+ * provide a customized version that deals with this problematic corner case.
+ */
+ static String stringOf(Object value) {
+ String s;
+ return (value == null || (s = value.toString()) == null) ? "null" : s;
+ }
+
private static final long LATIN1 = (long)String.LATIN1 << 32;
private static final long UTF16 = (long)String.UTF16 << 32;
+ private static final Unsafe UNSAFE = Unsafe.getUnsafe();
+
+ /**
+ * Allocates an uninitialized byte array based on the length and coder information
+ * in indexCoder
+ * @param indexCoder
+ * @return the newly allocated byte array
+ */
+ @ForceInline
+ static byte[] newArray(long indexCoder) {
+ byte coder = (byte)(indexCoder >> 32);
+ int index = (int)indexCoder;
+ return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, index << coder);
+ }
+
/**
* Provides the initial coder for the String.
* @return initial coder, adjusted into the upper half
--- a/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java Tue Apr 16 21:29:33 2019 +0000
+++ b/src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java Wed Apr 17 00:06:38 2019 +0200
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2015, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2019, 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
@@ -26,6 +26,7 @@
package java.lang.invoke;
import jdk.internal.misc.Unsafe;
+import jdk.internal.misc.VM;
import jdk.internal.org.objectweb.asm.ClassWriter;
import jdk.internal.org.objectweb.asm.Label;
import jdk.internal.org.objectweb.asm.MethodVisitor;
@@ -191,6 +192,8 @@
*/
private static final ProxyClassesDumper DUMPER;
+ private static final Class<?> STRING_HELPER;
+
static {
// In case we need to double-back onto the StringConcatFactory during this
// static initialization, make sure we have the reasonable defaults to complete
@@ -202,15 +205,20 @@
// DEBUG = false; // implied
// DUMPER = null; // implied
- Properties props = GetPropertyAction.privilegedGetProperties();
+ try {
+ STRING_HELPER = Class.forName("java.lang.StringConcatHelper");
+ } catch (Throwable e) {
+ throw new AssertionError(e);
+ }
+
final String strategy =
- props.getProperty("java.lang.invoke.stringConcat");
+ VM.getSavedProperty("java.lang.invoke.stringConcat");
CACHE_ENABLE = Boolean.parseBoolean(
- props.getProperty("java.lang.invoke.stringConcat.cache"));
+ VM.getSavedProperty("java.lang.invoke.stringConcat.cache"));
DEBUG = Boolean.parseBoolean(
- props.getProperty("java.lang.invoke.stringConcat.debug"));
+ VM.getSavedProperty("java.lang.invoke.stringConcat.debug"));
final String dumpPath =
- props.getProperty("java.lang.invoke.stringConcat.dumpClasses");
+ VM.getSavedProperty("java.lang.invoke.stringConcat.dumpClasses");
STRATEGY = (strategy == null) ? DEFAULT_STRATEGY : Strategy.valueOf(strategy);
CACHE = CACHE_ENABLE ? new ConcurrentHashMap<>() : null;
@@ -1519,6 +1527,33 @@
static MethodHandle generate(MethodType mt, Recipe recipe) throws Throwable {
+ // Fast-path two-argument Object + Object concatenations
+ if (recipe.getElements().size() == 2) {
+ // Two object arguments
+ if (mt.parameterCount() == 2 &&
+ !mt.parameterType(0).isPrimitive() &&
+ !mt.parameterType(1).isPrimitive()) {
+ return SIMPLE;
+ }
+ // One element is a constant
+ if (mt.parameterCount() == 1 && !mt.parameterType(0).isPrimitive()) {
+ MethodHandle mh = SIMPLE;
+ // Insert constant element
+
+ // First recipe element is a constant
+ if (recipe.getElements().get(0).getTag() == TAG_CONST &&
+ recipe.getElements().get(1).getTag() != TAG_CONST) {
+ return MethodHandles.insertArguments(mh, 0,
+ recipe.getElements().get(0).getValue());
+ } else if (recipe.getElements().get(1).getTag() == TAG_CONST &&
+ recipe.getElements().get(0).getTag() != TAG_CONST) {
+ return MethodHandles.insertArguments(mh, 1,
+ recipe.getElements().get(1).getValue());
+ }
+ // else... fall-through to slow-path
+ }
+ }
+
// Create filters and obtain filtered parameter types. Filters would be used in the beginning
// to convert the incoming arguments into the arguments we can process (e.g. Objects -> Strings).
// The filtered argument type list is used all over in the combinators below.
@@ -1626,13 +1661,6 @@
return mh;
}
- @ForceInline
- private static byte[] newArray(long indexCoder) {
- byte coder = (byte)(indexCoder >> 32);
- int index = (int)indexCoder;
- return (byte[]) UNSAFE.allocateUninitializedArray(byte.class, index << coder);
- }
-
private static MethodHandle prepender(Class<?> cl) {
return PREPENDERS.computeIfAbsent(cl, PREPEND);
}
@@ -1659,16 +1687,15 @@
}
};
+ private static final MethodHandle SIMPLE;
private static final MethodHandle NEW_STRING;
private static final MethodHandle NEW_ARRAY;
private static final ConcurrentMap<Class<?>, MethodHandle> PREPENDERS;
private static final ConcurrentMap<Class<?>, MethodHandle> MIXERS;
private static final long INITIAL_CODER;
- static final Class<?> STRING_HELPER;
static {
try {
- STRING_HELPER = Class.forName("java.lang.StringConcatHelper");
MethodHandle initCoder = lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "initialCoder", long.class);
INITIAL_CODER = (long) initCoder.invoke();
} catch (Throwable e) {
@@ -1678,8 +1705,9 @@
PREPENDERS = new ConcurrentHashMap<>();
MIXERS = new ConcurrentHashMap<>();
+ SIMPLE = lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "simpleConcat", String.class, Object.class, Object.class);
NEW_STRING = lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "newString", String.class, byte[].class, long.class);
- NEW_ARRAY = lookupStatic(Lookup.IMPL_LOOKUP, MethodHandleInlineCopyStrategy.class, "newArray", byte[].class, long.class);
+ NEW_ARRAY = lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "newArray", byte[].class, long.class);
}
}
@@ -1692,22 +1720,8 @@
// no instantiation
}
- private static class ObjectStringifier {
-
- // We need some additional conversion for Objects in general, because String.valueOf(Object)
- // may return null. String conversion rules in Java state we need to produce "null" String
- // in this case, so we provide a customized version that deals with this problematic corner case.
- private static String valueOf(Object value) {
- String s;
- return (value == null || (s = value.toString()) == null) ? "null" : s;
- }
-
- // Could have used MethodHandles.lookup() instead of Lookup.IMPL_LOOKUP, if not for the fact
- // java.lang.invoke Lookups are explicitly forbidden to be retrieved using that API.
- private static final MethodHandle INSTANCE =
- lookupStatic(Lookup.IMPL_LOOKUP, ObjectStringifier.class, "valueOf", String.class, Object.class);
-
- }
+ private static final MethodHandle OBJECT_INSTANCE =
+ lookupStatic(Lookup.IMPL_LOOKUP, STRING_HELPER, "stringOf", String.class, Object.class);
private static class FloatStringifiers {
private static final MethodHandle FLOAT_INSTANCE =
@@ -1751,7 +1765,7 @@
*/
static MethodHandle forMost(Class<?> t) {
if (!t.isPrimitive()) {
- return ObjectStringifier.INSTANCE;
+ return OBJECT_INSTANCE;
} else if (t == float.class) {
return FloatStringifiers.FLOAT_INSTANCE;
} else if (t == double.class) {
--- a/test/micro/org/openjdk/bench/java/lang/StringConcat.java Tue Apr 16 21:29:33 2019 +0000
+++ b/test/micro/org/openjdk/bench/java/lang/StringConcat.java Wed Apr 17 00:06:38 2019 +0200
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2018 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2018, 2019, 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
@@ -63,6 +63,11 @@
}
@Benchmark
+ public String concatMethodConstString() {
+ return "string".concat(stringValue);
+ }
+
+ @Benchmark
public String concatConstIntConstInt() {
return "string" + intValue + "string" + intValue;
}