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/