I agree that getting rid of the append(concatenation) is a good move regardless; that’s just wasteful movement. The rest of the patch seems harmless.
As to applying the refactor more broadly, there’s a risk of recreating the “append(concatenation)” problem, where the concatenation is hidden inside the call to repeat(). A loop over append() is likely to be more performant than append(s.repeat()), even those the latter is more abstract and harder to get wrong. If you’re willing to take the auto-refactor result and hand-edit it to eliminate the cases where we’d create new append(concat) situations, I’d be supportive of that as well. > On Mar 26, 2019, at 4:15 PM, Сергей Цыпанов <[email protected]> wrote: > > 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" <[email protected]>: >> 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, "Сергей Цыпанов" <[email protected]>: >>>> 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 > <klass.txt>
