Hi Claes,

On 03/30/2016 06:17 PM, Claes Redestad wrote:
Hi Peter,

something like this, then:

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


Perfect.

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

What happens if you throw an exception in the plugin? Should jlink fail in case of invalid input?

Regards, Peter

- 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