Nice work, Claes!

j.l.i part looks fine.

Best regards,
Vladimir Ivanov

On 3/30/16 7:17 PM, Claes Redestad wrote:
Hi Peter,

something like this, then:

http://cr.openjdk.java.net/~redestad/8152641/webrev.05/

- Added a method only used by the plugin which validates input; added a
comment about the dependency
- Invalid types are logged but otherwise ignored now (I bet someone
might suggest a better way to handle user errors)
- Some cleanup, introduced constant for class name prefix and removed
duplicate type string shortening etc

/Claes

On 2016-03-30 17:17, Peter Levart wrote:
Hi Claes,

webrev.04 looks good now.

Maybe just one nit. For production quality, plugin input could be
verified that after expansion it is composed of just the following
characters: "LIJFD". Otherwise ClassWriter might generate an unusable
class without complaining (for example if someone sneaks-in characters
'S' 'Z' 'B' or 'C')...

Or, better yet, create another method in BMH that will be the "public"
API between the plugin and BMH which does the validation and calls
generateConcreteBMHClassBytes(). Internally in BMH the validation is
not necessary as the types strings are composed programmatically and
are guaranteed to be correct.

Regards, Peter

On 03/30/2016 04:15 PM, Claes Redestad wrote:


On 2016-03-30 14:21, Peter Levart wrote:
Hi Claes,

On 03/30/2016 12:53 PM, Claes Redestad wrote:
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.

Good idea.


If I call this final, will you say "Reviewed"? :-)

Sorry, I don't posses the powers. :-)

I could say "rEVIEWED", but...

In the plugin, the input is shortened speciesTypes strings. What
guarantees that they really are normalized? For example, If one
specifies "LLL" as input, it will get expanded into "LLL", the
generated class will have "_L3" as a name suffix, but you will pack
it in the image with "_LLL.class" filename suffix.

That's another reason why a method in BoundMethodHandle$Factory with
the following signature:

Map.Entry<String, byte[]> generateConcreteBMHClassBytes(String types);

...would be a good idea. It would return class bytes and the name of
the class which you could use to derive the class filename without
hardcoding the same logic in plugin and in BMH.

You just move the "LambdaForm.shortenSignature(types)" from
getConcreteBMHClass and className/sourceFile calculation from
generateConcreteBMHClass down to generateConcreteBMHClassBytes
method and change the signatures...

Yes, it makes sense to keep control over the class name inside the
factory class, and this does allow specifying shortened or expanded
forms (L3 vs LLL) interchangeably as input to the plugin, which
reduces possibility for user errors.

How about this: http://cr.openjdk.java.net/~redestad/8152641/webrev.04/

/Claes


Regards, Peter


/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/







Reply via email to