Hi Claes, Did you considere to use return c.asSubclass(BoundMethodHandle.class); instead of return (Class<? extends BoundMethodHandle>)c; to avoid the @SupressWarnings, like in generateConcreteBMHClass ?
cheers, Rémi ----- Mail original ----- > De: "Claes Redestad" <claes.redes...@oracle.com> > À: "Peter Levart" <peter.lev...@gmail.com>, "Mandy Chung" > <mandy.ch...@oracle.com> > Cc: "jigsaw-dev" <jigsaw-...@openjdk.java.net>, "core-libs-dev" > <core-libs-dev@openjdk.java.net> > Envoyé: Mercredi 30 Mars 2016 12:53:08 > Objet: Re: RFR: 8152641: Plugin to generate BMH$Species classes ahead-of-time > > Hi, > > I think Class.forName(Module, String) seemed like a nice > efficiency/simplicity compromise, but there is some usage of > lambdas/method-refs in the Module lookup code, especially for exploded > modules (which get exercised during JDK build). Depending on a lambda > from code in java.lang.invoke means we fail to bootstrap. > > But hey, we're living in an encapsulated world now, and this is in > java.base, so why not go directly to the BootLoader: > > http://cr.openjdk.java.net/~redestad/8152641/webrev.03/ > > Since this is what's called from Class.forName(Module, String), the > overhead should be even smaller than in your classForNameInModule test. > > If I call this final, will you say "Reviewed"? :-) > > /Claes > > PS: The default list of types is generated in a few adhoc tests not part > of this patch. I'm considering proposing add support for generating this > list at build time. Maybe a JEP called "Build system support for > profile-guided optimizations", which could also handle > https://bugs.openjdk.java.net/browse/JDK-8150044 > > On 2016-03-30 09:53, Peter Levart wrote: > > Hi Claes, > > > > Sorry, I found a flaw in the benchmark (the regex pattern to split > > comma-separated string into substrings was wrong). What the benchmark > > did was compare the overheads of a single lookup of a longer class > > name containing commas. Here's the corrected result of overheads of 5 > > unsuccessful lookups: > > > > Benchmark (generate) (lookup) Mode Cnt > > Score Error Units > > ClassForNameBench.classForName LL,LLL,LLLL,LLLLL,LLLLLL > > LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 29627.141 ± 567.427 ns/op > > ClassForNameBench.classForNameInModule LL,LLL,LLLL,LLLLL,LLLLLL > > LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 1073.256 ± 23.794 ns/op > > ClassForNameBench.hashSetContains LL,LLL,LLLL,LLLLL,LLLLLL > > LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 33.022 ± 0.066 ns/op > > ClassForNameBench.switchStatement LL,LLL,LLLL,LLLLL,LLLLLL > > LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 38.498 ± 5.842 ns/op > > > > ...overheads are a little bigger (x5 approx.). > > > > > > Here's the corrected benchmark: > > > > > > package jdk.test; > > > > import org.openjdk.jmh.annotations.*; > > import org.openjdk.jmh.infra.Blackhole; > > > > import java.io.Serializable; > > import java.lang.invoke.MethodHandle; > > import java.util.Iterator; > > import java.util.Set; > > import java.util.concurrent.TimeUnit; > > import java.util.stream.Collectors; > > import java.util.stream.Stream; > > > > @BenchmarkMode(Mode.AverageTime) > > @Fork(value = 1, warmups = 0) > > @Warmup(iterations = 10) > > @Measurement(iterations = 10) > > @OutputTimeUnit(TimeUnit.NANOSECONDS) > > @State(Scope.Thread) > > public class ClassForNameBench { > > > > static final String BMH = "java/lang/invoke/BoundMethodHandle"; > > static final String SPECIES_PREFIX_NAME = "Species_"; > > static final String SPECIES_PREFIX_PATH = BMH + "$" + > > SPECIES_PREFIX_NAME; > > > > @Param({"LL,LLL,LLLL,LLLLL,LLLLLL"}) > > public String generate; > > > > @Param({"LLI,LLLI,LLLLI,LLLLLI,LLLLLLI"}) > > public String lookup; > > > > private String[] generatedTypes; > > private String[] lookedUpTypes; > > private Set<String> generatedNames; > > private String[] lookedUpNames; > > > > @Setup(Level.Trial) > > public void setup() { > > generatedTypes = generate.trim().split("\\s*,\\s*"); > > lookedUpTypes = lookup.trim().split("\\s*,\\s*"); > > generatedNames = Stream.of(generatedTypes) > > .map(types -> SPECIES_PREFIX_PATH + > > shortenSignature(types)) > > .collect(Collectors.toSet()); > > lookedUpNames = Stream.of(lookedUpTypes) > > .map(types -> SPECIES_PREFIX_PATH + > > shortenSignature(types)) > > .toArray(String[]::new); > > } > > > > @Benchmark > > public void classForName(Blackhole bh) { > > for (String name : lookedUpNames) { > > try { > > bh.consume(Class.forName(name, false, > > MethodHandle.class.getClassLoader())); > > } catch (ClassNotFoundException e) { > > bh.consume(e); > > } > > } > > } > > > > @Benchmark > > public void classForNameInModule(Blackhole bh) { > > for (String name : lookedUpNames) { > > bh.consume(Class.forName(MethodHandle.class.getModule(), name)); > > } > > } > > > > @Benchmark > > public void hashSetContains(Blackhole bh) { > > for (String name : lookedUpNames) { > > bh.consume(generatedNames.contains(name)); > > } > > } > > > > @Benchmark > > public void switchStatement(Blackhole bh) { > > for (String types : lookedUpTypes) { > > bh.consume(getBMHSwitch(types)); > > } > > } > > > > static Class<?> getBMHSwitch(String types) { > > // should be in sync with @Param generate above... > > switch (types) { > > case "LL": return Object.class; > > case "LLL": return Serializable.class; > > case "LLLL": return Iterator.class; > > case "LLLLL": return Throwable.class; > > case "LLLLLL": return String.class; > > default: return null; > > } > > } > > > > // copied from non-public LambdaForm > > static String shortenSignature(String signature) { > > // Hack to make signatures more readable when they show up in > > method names. > > final int NO_CHAR = -1, MIN_RUN = 3; > > int c0, c1 = NO_CHAR, c1reps = 0; > > StringBuilder buf = null; > > int len = signature.length(); > > if (len < MIN_RUN) return signature; > > for (int i = 0; i <= len; i++) { > > // shift in the next char: > > c0 = c1; c1 = (i == len ? NO_CHAR : signature.charAt(i)); > > if (c1 == c0) { ++c1reps; continue; } > > // shift in the next count: > > int c0reps = c1reps; c1reps = 1; > > // end of a character run > > if (c0reps < MIN_RUN) { > > if (buf != null) { > > while (--c0reps >= 0) > > buf.append((char) c0); > > } > > continue; > > } > > // found three or more in a row > > if (buf == null) > > buf = new StringBuilder().append(signature, 0, i - > > c0reps); > > buf.append((char) c0).append(c0reps); > > } > > return (buf == null) ? signature : buf.toString(); > > } > > } > > > > > > Regards, Peter > > > > > > > > On 03/30/2016 09:40 AM, Peter Levart wrote: > >> Hi Claes, > >> > >> On 03/30/2016 01:03 AM, Claes Redestad wrote: > >>> Hi Peter, Mandy, > >>> > >>> On 2016-03-26 12:47, Peter Levart wrote: > >>>> > >>>> Comparing this with proposed code from webrev: > >>>> > >>>> 493 try { > >>>> 494 return (Class<? extends > >>>> BoundMethodHandle>) > >>>> 495 Class.forName("java.lang.invoke.BoundMethodHandle$Species_" + > >>>> LambdaForm.shortenSignature(types)); > >>>> 496 } catch (ClassNotFoundException cnf) { > >>>> 497 // Not pregenerated, generate the > >>>> class > >>>> 498 return > >>>> generateConcreteBMHClass(types); > >>>> 499 } > >>>> > >>>> > >>>> ...note that the function passed to CLASS_CACHE.computeIfAbsent is > >>>> executed just once per distinct 'types' argument. Even if you put > >>>> the generated switch between a call to getConcreteBMHClass and > >>>> CLASS_CACHE.computeIfAbsent, getConcreteBMHClass(String types) is > >>>> executed just once per distinct 'types' argument (except in rare > >>>> occasions when VM can not initialize the loaded class). > >>>> > >>>> In this respect a successful Class.forName() is not any worse than > >>>> static resolving. It's just that unsuccessful Class.forName() > >>>> represents some overhead for classes that are not pre-generated. So > >>>> an alternative way to get rid of that overhead is to have a HashSet > >>>> of 'types' strings for pre-generated classes at hand in order to > >>>> decide whether to call Class.forName or generateConcreteBMHClass. > >>>> > >>>> What's easier to support is another question. > >>>> > >>>> Regards, Peter > >>> > >>> to have something to compare with I built a version which, like you > >>> suggest, > >>> generates a HashSet<String> with the set of generated classes here: > >>> > >>> http://cr.openjdk.java.net/~redestad/8152641/webrev.02/ > >>> > >>> This adds a fair bit of complexity to the plugin and requires we add > >>> a nested > >>> class in BoundMethodHandle that we can replace. Using the anonymous > >>> compute Function for this seems like the best choice for this. > >> > >> ...what I had in mind as alternative to a pregenerated class with a > >> switch was a simple textual resource file, generated by plugin, > >> read-in by BMH into a HashSet. No special-purpose class generation > >> and much less complexity for the plugin. > >> > >>> > >>> I've not been able to measure any statistical difference in real > >>> startup terms, > >>> though, and not figured out a smart way to benchmark the overhead of > >>> the > >>> CNFE in relation to the class generation (my guess it adds a > >>> fraction to the > >>> total cost) and since this adds ever so little footprint and an > >>> extra lookup to the > >>> fast path it would seem that this is the wrong trade-off to do here. > >> > >> Yes, perhaps it would be best to just use Class.forName(module, > >> className) instead. I have created a little benchmark to compare > >> overheads (just overheads) of unsuccessful lookups for pregenerated > >> classes (a situation where a BMH class is requested that has not been > >> pregenerated) and here's the result for overhead of 5 unsuccessfull > >> lookups: > >> > >> Benchmark (generate) (lookup) Mode Cnt > >> Score Error Units > >> ClassForNameBench.classForName LL,LLL,LLLL,LLLLL,LLLLLL > >> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 6800.878 ± 421.424 ns/op > >> ClassForNameBench.classForNameInModule LL,LLL,LLLL,LLLLL,LLLLLL > >> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 209.574 ± 2.114 ns/op > >> ClassForNameBench.hashSetContains LL,LLL,LLLL,LLLLL,LLLLLL > >> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 6.813 ± 0.317 ns/op > >> ClassForNameBench.switchStatement LL,LLL,LLLL,LLLLL,LLLLLL > >> LLI,LLLI,LLLLI,LLLLLI,LLLLLLI avgt 10 6.601 ± 0.061 ns/op > >> > >> ...compared to runtime BMH class generation and loading this is > >> really a very minor overhead. I would just use Class.forName(module, > >> className) and reduce the complexity of the plugin. > >> > >> What do you think? > >> > >> > >> Here's the benchmark: > >> > >> package jdk.test; > >> > >> import org.openjdk.jmh.annotations.*; > >> import org.openjdk.jmh.infra.Blackhole; > >> > >> import java.io.Serializable; > >> import java.lang.invoke.MethodHandle; > >> import java.util.Iterator; > >> import java.util.Set; > >> import java.util.concurrent.TimeUnit; > >> import java.util.stream.Collectors; > >> import java.util.stream.Stream; > >> > >> @BenchmarkMode(Mode.AverageTime) > >> @Fork(value = 1, warmups = 0) > >> @Warmup(iterations = 10) > >> @Measurement(iterations = 10) > >> @OutputTimeUnit(TimeUnit.NANOSECONDS) > >> @State(Scope.Thread) > >> public class ClassForNameBench { > >> > >> static final String BMH = "java/lang/invoke/BoundMethodHandle"; > >> static final String SPECIES_PREFIX_NAME = "Species_"; > >> static final String SPECIES_PREFIX_PATH = BMH + "$" + > >> SPECIES_PREFIX_NAME; > >> > >> @Param({"LL,LLL,LLLL,LLLLL,LLLLLL"}) > >> public String generate; > >> > >> @Param({"LLI,LLLI,LLLLI,LLLLLI,LLLLLLI"}) > >> public String lookup; > >> > >> private String[] generatedTypes; > >> private String[] lookedUpTypes; > >> private Set<String> generatedNames; > >> private String[] lookedUpNames; > >> > >> @Setup(Level.Trial) > >> public void setup() { > >> generatedTypes = generate.trim().split("\\s+,\\s+"); > >> lookedUpTypes = lookup.trim().split("\\s+,\\s+"); > >> generatedNames = Stream.of(generatedTypes) > >> .map(types -> SPECIES_PREFIX_PATH + > >> shortenSignature(types)) > >> .collect(Collectors.toSet()); > >> lookedUpNames = Stream.of(lookedUpTypes) > >> .map(types -> SPECIES_PREFIX_PATH + > >> shortenSignature(types)) > >> .toArray(String[]::new); > >> } > >> > >> @Benchmark > >> public void classForName(Blackhole bh) { > >> for (String name : lookedUpNames) { > >> try { > >> bh.consume(Class.forName(name, false, > >> MethodHandle.class.getClassLoader())); > >> } catch (ClassNotFoundException e) { > >> bh.consume(e); > >> } > >> } > >> } > >> > >> @Benchmark > >> public void classForNameInModule(Blackhole bh) { > >> for (String name : lookedUpNames) { > >> bh.consume(Class.forName(MethodHandle.class.getModule(), name)); > >> } > >> } > >> > >> @Benchmark > >> public void hashSetContains(Blackhole bh) { > >> for (String name : lookedUpNames) { > >> bh.consume(generatedNames.contains(name)); > >> } > >> } > >> > >> @Benchmark > >> public void switchStatement(Blackhole bh) { > >> for (String types : lookedUpTypes) { > >> bh.consume(getBMHSwitch(types)); > >> } > >> } > >> > >> static Class<?> getBMHSwitch(String types) { > >> // should be in sync with @Param generate above... > >> switch (types) { > >> case "LL": return Object.class; > >> case "LLL": return Serializable.class; > >> case "LLLL": return Iterator.class; > >> case "LLLLL": return Throwable.class; > >> case "LLLLLL": return String.class; > >> default: return null; > >> } > >> } > >> > >> // copied from non-public LambdaForm > >> static String shortenSignature(String signature) { > >> // Hack to make signatures more readable when they show up in > >> method names. > >> final int NO_CHAR = -1, MIN_RUN = 3; > >> int c0, c1 = NO_CHAR, c1reps = 0; > >> StringBuilder buf = null; > >> int len = signature.length(); > >> if (len < MIN_RUN) return signature; > >> for (int i = 0; i <= len; i++) { > >> // shift in the next char: > >> c0 = c1; c1 = (i == len ? NO_CHAR : signature.charAt(i)); > >> if (c1 == c0) { ++c1reps; continue; } > >> // shift in the next count: > >> int c0reps = c1reps; c1reps = 1; > >> // end of a character run > >> if (c0reps < MIN_RUN) { > >> if (buf != null) { > >> while (--c0reps >= 0) > >> buf.append((char) c0); > >> } > >> continue; > >> } > >> // found three or more in a row > >> if (buf == null) > >> buf = new StringBuilder().append(signature, 0, i - > >> c0reps); > >> buf.append((char) c0).append(c0reps); > >> } > >> return (buf == null) ? signature : buf.toString(); > >> } > >> } > >> > >> > >> > >> Regards, Peter > >> > >>> > >>> All-in-all I lean towards moving forward with the first, simpler > >>> revision of this > >>> patch[1] and then evaluate if webrev.02 or a String-switch+static > >>> resolve > >>> as a follow-up. > >>> > >>> A compromise would be to keep the SpeciesLookup class introduced here > >>> to allow removing all overhead in case the plugin is disabled. > >>> > >>> Mandy: I've not found any test (under jdk/test/tools/jlink or > >>> elsewhere) which > >>> has to be updated when adding a plugin like this. > >>> > >>> Thanks! > >>> > >>> /Claes > >>> > >>> [1] http://cr.openjdk.java.net/~redestad/8152641/webrev.01/ > >> > > > >