Hello Peter,

I was unaware of mentioned detail (thank you a lot for pointing that out, it 
made me read the whole JEP280 again) so I've rebenchmarked my changes compiled 
with -XDstringConcat=inline.

1) as of Class::getTypeName for Object[] it turned out, that String::repeat 
performs slightly better:

Benchmark                                                     Mode  Cnt     
Score     Error   Units

getTypeName_patched                                           avgt   25    
36,676 ±   3,559   ns/op
getTypeName                                                   avgt   25    
39,083 ±   1,737   ns/op

getTypeName_patched:·gc.alloc.rate.norm                       avgt   25   
152,000 ±   0,001    B/op
getTypeName:·gc.alloc.rate.norm                               avgt   25   
152,000 ±   0,001    B/op


But for Object[][] situation is the opposite:

                                                            Mode  Cnt     Score 
    Error   Units

getTypeName_patched                                         avgt   50    47,045 
±   0,455   ns/op
getTypeName                                                 avgt   50    40,536 
±   0,196   ns/op

getTypeName_patched:·gc.alloc.rate.norm                     avgt   50   176,000 
±   0,001    B/op
getTypeName:·gc.alloc.rate.norm                             avgt   50   152,000 
±   0,001    B/op


2) as of Class::methodToString patched version clearly wins:

methodToString_1arg                                         avgt   50   238,476 
±   1,641   ns/op
methodToString_1arg_patched                                 avgt   50   197,900 
±   5,824   ns/op

methodToString_1arg:·gc.alloc.rate.norm                     avgt   50  1209,600 
±   6,401    B/op
methodToString_1arg_patched:·gc.alloc.rate.norm             avgt   50   936,000 
±   0,001    B/op

methodToString_noArgs                                       avgt   50   224,054 
±   1,840   ns/op
methodToString_noArgs_patched                               avgt   50    78,195 
±   0,418   ns/op

methodToString_noArgs:·gc.alloc.rate.norm                   avgt   50  1131,200 
±   7,839    B/op
methodToString_noArgs_patched:·gc.alloc.rate.norm           avgt   50   408,000 
±   0,001    B/op


As Brian suggested I've separated the changes. The patch attached contains only 
the changes for Class::methodToString. They are obviously helpful as they rid 
concatenation as argument of StringBuilder::append call (obvious antipattern to 
me) and skip Stream creation for empty array.

String::repeat obviously requires more investigation, it might turn out we 
don't need it in java.base in majority of use cases or in all of them.

Regards,
Sergei Tsypanov



21.03.2019, 00:56, "Peter Levart" <peter.lev...@gmail.com>:
> Hi Sergei,
>
> I don't know if you are aware that the new invokedynamic based
> translation of string concatenation expressions introduced in JDK 9
> (http://openjdk.java.net/jeps/280) only applies to code outside
> java.base module. java.base module is still compiled with old-style
> StringBuilder based translation of string concatenation expressions
> because of bootstraping issues.
>
> If your benchmarks bellow are compiled with option
> -XDstringConcat=inline to disable JEP280 translation (as java.base
> module is), then it's OK. Else you are not benchmarking the right
> translation of the code as you intend to change the code in java.base
> module.
>
> Regards, Peter
>
> On 3/20/19 7:35 PM, Сергей Цыпанов wrote:
>>  I had a brief conversation with Brian Goetz which has left off the mailing 
>> list for some reason. Here's the text:
>>  ---------------------------
>>  Brian:
>>
>>  These enhancements seem reasonable; these are exactly the cases that 
>> String::repeat was intended for. (This is a “is this a reasonable idea” 
>> review, not a code review.).
>>  Have you looked for other places where similar transformations could be 
>> done?
>>
>>  ---------------------------
>>  Me:
>>
>>  Hello,
>>  after I had realized that looped StringBuilder::append calls can sometimes 
>> be replaced with String::repeat I requested corresponding inspection in IDEA 
>> issue tracker.
>>  Using the inspection I’ve found 129 similar warnings in jdk13 repo.
>>  Some of them are very promising, e.g. BigDecimal:: toPlainString where 
>> currently we have
>>
>>>  int trailingZeros = checkScaleNonZero((-(long)scale));
>>>  StringBuilder buf;
>>>  if(intCompact!=INFLATED) {
>>>       buf = new StringBuilder(20+trailingZeros);
>>>       buf.append(intCompact);
>>>  } else {
>>>       String str = intVal.toString();
>>>       buf = new StringBuilder(str.length()+trailingZeros);
>>>       buf.append(str);
>>>  }
>>>  for (int i = 0; i < trailingZeros; i++) {
>>>       buf.append('0');
>>>  }
>>>  return buf.toString();
>>  which in fact can be simplified to
>>
>>>  int trailingZeros = checkScaleNonZero((-(long)scale));
>>>  if(intCompact!=INFLATED) {
>>>       return intCompact + "0".repeat(trailingZeros);
>>>  } else {
>>>       return intVal.toString() + "0".repeat(trailingZeros);
>>>  }
>>  The second solution is not only shorter and more simple but it likely to be 
>> significantly faster and memory-saving.
>>  BTW, your reply to original message for some reason is missing from 
>> web-view.
>>
>>  ---------------------------
>>  Brian:
>>
>>  Cool. Note that replacing append() calls with repeat is not necessarily a 
>> win for anything other than code compactness; String::repeat will create a 
>> new string, which will then get appended to another StrinBuilder. So when 
>> used correctly (such as presized StringBuilders) appending in a loop is 
>> probably just as fast (look at the implementation of String::repeat.).
>>
>>>  if(intCompact!=INFLATED) {
>>>       return intCompact + "0".repeat(trailingZeros);
>>>  } else {
>>>       return intVal.toString() + "0".repeat(trailingZeros);
>>>  }
>>  Which can be further simplified to
>>
>>>      ((intCompact != INFLATED) ? intCompact : intVal.toString) + 
>>> “0”.repeat(trailingZeros);
>>
>>  The second solution is not only shorter and more simple but it likely to be 
>> significantly faster and memory-saving.
>>  I like short and simple. I am questioning the “faster and memory saving.” 
>> That should be validated.
>>
>>  ---------------------------
>>  Me:
>>
>>  I think optimizations provided by JEP 280 allow compiler to optimize String 
>> concatenation executed with '+' by using StringBuilder of exact size (when 
>> the size of all components is known, like in our case).
>>
>>  I've checked this with benchmark
>>
>>>  @State(Scope.Thread)
>>>  @BenchmarkMode(Mode.AverageTime)
>>>  @OutputTimeUnit(TimeUnit.NANOSECONDS)
>>>  @Fork(jvmArgsAppend = {"-Xms2g", "-Xmx2g"})
>>>  public class ConcatBenchmark {
>>>
>>>       @Param({"1", "2", "5", "8"})
>>>       private int trailingZeros;
>>>
>>>       private final long intCompact = 1L;
>>>
>>>       @Benchmark
>>>       public String stringBuilder() {
>>>           StringBuilder buf;
>>>           buf = new StringBuilder(20 + trailingZeros);
>>>           buf.append(intCompact);
>>>           for (int i = 0; i < trailingZeros; i++) {
>>>               buf.append('0');
>>>           }
>>>           return buf.toString();
>>>       }
>>>       @Benchmark
>>>       public String stringRepeat() {
>>>           return intCompact + "0".repeat(trailingZeros);
>>>       }
>>>  }
>>  Results:
>>                                           trailingZeros Mode Cnt Score Error 
>> Units
>>  stringBuilder 1 avgt 15 17,683 ± 0,664 ns/op
>>  stringRepeat 1 avgt 15 9,052 ± 0,042 ns/op
>>
>>  stringBuilder 2 avgt 15 19,053 ± 1,206 ns/op
>>  stringRepeat 2 avgt 15 15,864 ± 1,331 ns/op
>>
>>  stringBuilder 5 avgt 15 25,839 ± 2,256 ns/op
>>  stringRepeat 5 avgt 15 19,290 ± 1,685 ns/op
>>
>>  stringBuilder 8 avgt 15 26,488 ± 0,371 ns/op
>>  stringRepeat 8 avgt 15 19,420 ± 1,593 ns/op
>>
>>  stringBuilder:·gc.alloc.rate.norm 1 avgt 15 88,000 ± 0,001 B/op
>>  stringRepeat:·gc.alloc.rate.norm 1 avgt 15 48,000 ± 0,001 B/op
>>
>>  stringBuilder:·gc.alloc.rate.norm 2 avgt 15 88,000 ± 0,001 B/op
>>  stringRepeat:·gc.alloc.rate.norm 2 avgt 15 72,000 ± 0,001 B/op
>>
>>  stringBuilder:·gc.alloc.rate.norm 5 avgt 15 96,000 ± 0,001 B/op
>>  stringRepeat:·gc.alloc.rate.norm 5 avgt 15 72,000 ± 0,001 B/op
>>
>>  stringBuilder:·gc.alloc.rate.norm 8 avgt 15 104,000 ± 0,001 B/op
>>  stringRepeat:·gc.alloc.rate.norm 8 avgt 15 80,000 ± 0,001 B/op
>>
>>  I think this is a useful optimization, so we should use String::repeat 
>> where possible.
>>
>>  ---------------------------
>>  Brian:
>>
>>  My recommendation is to split your patch into two. The first is the 
>> straightforward “replace loop with repeat” refactoring, which can be 
>> mechanically generated by the IDE. Here, you should examine each site to 
>> ensure that the transform seems sensible given the context. The second can 
>> then be additional hand-refactoring opportunities exposed by the first. The 
>> benefit of splitting it this way is that reviewing the first is far easier 
>> when you can say all the changes are mechanical.
>>
>>  (Somehow this fell off the list; feel free to bring it back.)
>>
>>  18.03.2019, 21:13, "Сергей Цыпанов" <sergei.tsypa...@yandex.ru>:
>>>  Hi,
>>>
>>>  I have an enhancement proposal for java.lang.Class.methodToString and 
>>> java.lang.Class.getTypeName.
>>>
>>>  First one is used when NoSuchMethodException is thrown from 
>>> Class::getMethod which is in turn widely used in Spring Framework and often 
>>> throws.
>>>
>>>  In current implementation we have 2 major problems:
>>>
>>>  - we create stream for the case when argTypes is not null but empty (which 
>>> happens e. g. when Class::getMethod is called without vararg and throws)
>>>  - we have torn StringBuilder::append chain
>>>
>>>  I’ve modified the method to skip creation of Stream for empty array and 
>>> used separate StringBuilder for each case. Latter allowed to rid SB 
>>> completely and use invokedynamic-based concatenation.
>>>
>>>  I’ve compared both approaches for 2 cases:
>>>
>>>  1) argTypes is empty
>>>  2) argTypes.length == 1
>>>
>>>  Benchmark Mode Cnt Score Error Units
>>>
>>>  methodToString_noArgs avgt 25 170,986 ± 5,708 ns/op
>>>  methodToString_noArgs_patched avgt 25 26,883 ± 2,906 ns/op
>>>
>>>  methodToString_1arg avgt 25 183,012 ± 0,701 ns/op
>>>  methodToString_1arg_patched avgt 25 112,784 ± 0,920 ns/op
>>>
>>>  methodToString_noArgs:·gc.alloc.rate.norm avgt 25 881,600 ± 9,786 B/op
>>>  methodToString_noArgs_patched:·gc.alloc.rate.norm avgt 25 128,000 ± 0,001 
>>> B/op
>>>
>>>  methodToString_1arg:·gc.alloc.rate.norm avgt 25 960,000 ± 0,001 B/op
>>>  methodToString_1arg_patched:·gc.alloc.rate.norm avgt 25 552,000 ± 0,001 
>>> B/op
>>>
>>>  We have the same problem regarding misusage of StringBuilder in Class:: 
>>> getTypeName:
>>>
>>>  StringBuilder sb = new StringBuilder();
>>>  sb.append(cl.getName());
>>>  for (int i = 0; i < dimensions; i++) {
>>>       sb.append("[]");
>>>  }
>>>  return sb.toString();
>>>
>>>  I suggest to use String::repeat instead of the loop: this again allows to 
>>> get rid of StringBuilder and replace mentioned code with
>>>
>>>  return cl.getName() + "[]".repeat(dimensions);
>>>
>>>  Here are benchmark results executed for type Object[].class:
>>>
>>>                                             Mode Cnt Score Error Units
>>>  getTypeName_patched avgt 25 16,037 ± 0,431 ns/op
>>>  getTypeName_patched:·gc.alloc.rate.norm avgt 25 64,000 ± 0,001 B/op
>>>
>>>  getTypeName avgt 25 34,274 ± 1,432 ns/op
>>>  getTypeName:·gc.alloc.rate.norm avgt 25 152,000 ± 0,001 B/op
>>>
>>>  Regards,
>>>  Sergei Tsypanov
diff --git a/src/java.base/share/classes/java/lang/Class.java b/src/java.base/share/classes/java/lang/Class.java
--- a/src/java.base/share/classes/java/lang/Class.java
+++ b/src/java.base/share/classes/java/lang/Class.java
@@ -3417,14 +3417,13 @@
      * Helper method to get the method name from arguments.
      */
     private String methodToString(String name, Class<?>[] argTypes) {
-        StringBuilder sb = new StringBuilder();
-        sb.append(getName() + "." + name + "(");
-        if (argTypes != null) {
-            sb.append(Stream.of(argTypes).map(c -> {return (c == null) ? "null" : c.getName();}).
-                      collect(Collectors.joining(",")));
+        if (argTypes == null || argTypes.length == 0) {
+            return getName() + '.' + name + "()";
         }
-        sb.append(")");
-        return sb.toString();
+        return getName() + '.' + name
+                + Arrays.stream(argTypes)
+                        .map(c -> c == null ? "null" : c.getName())
+                        .collect(Collectors.joining(",", "(", ")"));
     }
 
     /** use serialVersionUID from JDK 1.1 for interoperability */

Reply via email to