# HG changeset patch # User redestad # Date 1555452398 -7200 # Node ID 5fa7fbddfe9d7eba2600d9acbe0bb29003087677 # Parent 02ef8685889692a444cb2068c1686a857b3275be 8222484: Specialize generation of simple String concatenation expressions Reviewed-by: jrose, jlaskey diff -r 02ef86858896 -r 5fa7fbddfe9d make/jdk/src/classes/build/tools/classlist/HelloClasslist.java --- 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"))); diff -r 02ef86858896 -r 5fa7fbddfe9d src/java.base/share/classes/java/lang/String.java --- 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); } /** diff -r 02ef86858896 -r 5fa7fbddfe9d src/java.base/share/classes/java/lang/StringConcatHelper.java --- 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 diff -r 02ef86858896 -r 5fa7fbddfe9d src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java --- 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, MethodHandle> PREPENDERS; private static final ConcurrentMap, 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) { diff -r 02ef86858896 -r 5fa7fbddfe9d test/micro/org/openjdk/bench/java/lang/StringConcat.java --- 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; }