8222484: Specialize generation of simple String concatenation expressions
authorredestad
Wed, 17 Apr 2019 00:06:38 +0200
changeset 54550 5fa7fbddfe9d
parent 54549 02ef86858896
child 54551 6f8a7671afef
8222484: Specialize generation of simple String concatenation expressions Reviewed-by: jrose, jlaskey
make/jdk/src/classes/build/tools/classlist/HelloClasslist.java
src/java.base/share/classes/java/lang/String.java
src/java.base/share/classes/java/lang/StringConcatHelper.java
src/java.base/share/classes/java/lang/invoke/StringConcatFactory.java
test/micro/org/openjdk/bench/java/lang/StringConcat.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")));
--- 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;
     }