-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Finish the experimental method handle support #1894
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Thanks Luke! Will try to pull and test, and will review here soon! |
Welp I couldn't get bazel working and it shows... lol |
500060f
to
eee86fa
Compare
Fixed the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(approving so i can pull and validate internally, still need to review properly)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
General question: I'm curious about the caching strategy. AIUI, the cache for a given MH is unique per InternalFactory, but InternalFactory isn't widely shared (they are unique per binding IIRC). Is this improving cachability (and repeated injector constructions in tests, etc..) because we're removing the custom bytecode-generated Provider class? It seems like each distinct Injector will still have its own unique MethodHandles... but I guess that's more lightweight than a whole class definition per Provider? What value is the MH cacheability actually giving us if its unique per binding (or am I misunderstanding the scope of the caching)?
core/src/com/google/inject/internal/DefaultConstructionProxyFactory.java
Outdated
Show resolved
Hide resolved
@@ -345,8 +345,12 @@ private static Stream<Class<? extends Annotation>> getPermits(Class<?> clazz) { | |||
Stream.concat( | |||
annotations, Arrays.stream(clazz.getAnnotatedSuperclass().getAnnotations())); | |||
} | |||
return annotations |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I kept getting a build error here from maven
Type mismatch: cannot convert from Stream<Class<capture#26-of ? extends Annotation>> to Stream<Class<? extends Annotation>>Java(16777235)
Introducing this expliocit cast resolved it, so i guess i cannot figure out why this isn't a problem at head?
im guessing it is because i am using a different version of javac
% javac --version
javac 21.0.6
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok, i was confused before. this appears to be an incompatibiltiy with ecj
which is used by the vscode java plugins... sigh. It looks like there are number of issues where ecj reports and error and javac does not (and visa versa).
Added a comment to that effect. I can revert if you prefer
(Also, I ran through the depot last night, but forgot to flip the "enable this" flag to true... so running again now. Will see what it shakes out.) |
depot is fairly happy. Some things that surfaced:
|
So you understand the caching correctly. And indeed this will not address overhead in the case where a user constructs 2 identical or nearly identical injectors. However within the scope of a single injector this caching is useful since we will call e.g.
We will produce 2 So this is still valuable within the scope of an injector. Not every binding is consumed multiple times, but some are and we don't want to recompute those. So this really isn't improving repeated injector construction in tests. It might help a little just since creating handles for each injector is a little cheaper, but it doesn't enable cross injector sharing. The thing that does help the 'multiple injectors in a test' thing is dropping the bytecode generation since that was happening eagerly during injector creation (due to scopes), but now handle creation happens on demand. Cross injector sharing is a compelling idea and it is something we do get with our fast-class code generation but not with MethodHandles. Im really not sure how to actually achieve it. We could create a global interning map for thoughts? |
eee86fa
to
18cb790
Compare
Ah rats, forgot about this one. Will update with a test and a fix. I agree this seems odd
Interesting. I wonder if it is the allocations related to the lazy MethodHandles construction? That only happens when they actually call |
…ime value' Initially I thought it would make sense to inspect `Dependency` at link time (during `getHandle` calls), since it would allow us to trim null checks and optimize checks for proxies. However, both of those ideas turned out to not be very fruitful. * It is true that if an injection point was annotated as a`@Nullable` we could elide any null checks which is nice, but `@Nullable` injection points and nullish values are rare so optimizing for them is not useful. * I initially thought that if we called `tryStartConstruction` for a non-interface `Dependency` then we could not even bother checking the return value since a proxy was impossible. However, `ConstructorBinding` implementations can use the `finishConstructionAndSetReference` to satisfy re-entrant construction without a constructing a proxy. So this wasn't sufficient information. So the benefits of having `Dependency` available at link-time turned out to be either unachievable or very minor. On the other side the costs were high: * This necessitated a complex solution for Scoped providers (deleted in this commit) * This would interfere with future attempts to have `InternalFactory` types cache their factories.
The ideal way to invoke a method handle is by binding it to an `invokedynamic` instruction. This provides the VM with a lot of information about the lifetime and target of the methodhandle. On the other hand calling `invokeExact` on a more 'dynamic' methodhandle is slower since the VM always needs to do things like verify that the signature of the callsite matches the handle, and then resolve to the actual target. See https://wiki.openjdk.org/display/HotSpot/Deconstructing+MethodHandles for details. This is why the methodhandle implementation initially pursued bytecode generation, simply so we could bind to an `invokedynamic` instruction. However, this comes with a massive downside of needing to generate tons of classes. This slows down injector creation substantially and puts pressure on the vms `Compressed class space` in larger applications which limits scalability of this solution. So mitigate those risks this commit undoes the bytecode generation. A compromise solution would probably be some kind of `JIT` approch where we generate a custom provider once it gets hot enough so we would only bother with those Provider instances that get invoked a lot. However, im guessing the small perf penalty of `invokeExact` on a non-constant handle will be small and fixed and such it probably won't be too impactful in the limit.
…rastructure Now that we no longer generate Provider subclasses for MethodHandle support this isn't needed.
All factories have implemented this method now and it is no longer needed.
This will actualy improve performance on its own since `invokevirtual` is faster than `invokeinterface` for megamorphic callsites (of which many of ours are). This will also provide a natural place to add a method handle cache in the future
…ase class and simplify the protocol This is a tiny simplification and provides a simple consistent strategy for handling re-entrancy into the factory.
18cb790
to
fe79ddf
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(marking the latest iteration approved again, to be able to pull/test it.)
63fe55e
to
52be370
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
approving to pull internally again...
Each method handle is potentially compiled to a concrete java method and so we want to avoid recreating identical handles. This should help preserve VM resources like 'compressed class space' and improve linkage performance. The caching strategy is a little complicated since we need to respect the `linked` boolean parameter. Most factories ignore this parameter but some respect it so our caching strategy allows for both.
How valuable this is is somewhat debatable, but it is necessary if we want to make the fast-class support completely optional in the future. This revealed a number of latent issues I am addressing at the same time. Notably, some of the `asCollector` calls used for the fast-class fallbacks were not correct so this just eliminates all of them.
Most of the work was already done for ConstructorBindings but this required a different organization of the code and some caching. With this I believe everything is re-implemented in terms of MethodHandles.
…hen method handles are in use. Guice at head handles all `Errors` thrown by user code and wraps them in `ProvisionException` unless it is thrown by a ProviderInstanceBinding in which case it propagates unwrapped.
…d `ImmutableSet` objects Some investigation in https://stackoverflow.com/questions/79551257/efficiently-build-a-large-array-with-methodhandles revealed that the simple recursion approach was often the best so all approaches were simplified to do that. For the ImmutableMap/ImmutableSet cases, special cases were added for the `of` overloads that are supported by guava. From perusing the source these trigger fast paths and allow us to avoid the `Builder` allocation.
…our `catch` clauses for constructor bindings Fix a similar issue with @Inject methods Working on this reveals a behavior difference in exception reproting between `@Inject` constructors and `@Provides` methods, so i added a test to demonstrate that as well. This is easy to fix but not a big deal.
…e to the reflection API
52be370
to
2166e12
Compare
Just repushed to fix the javadoc issue flagged by bazel CI... sorry i guess ill try to get the bazel build working locally again |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(approving to pull to internal repo)
Re: this case, the path causing issues seems to follow the following chain of logic:
That seems to trigger
(note the line # is from a prior snapshot, not the latest) So either something in per-request Injector.getBinding().getProvider().get() is defeating MH caching, or the use of (FYI I am probably going to submit this PR as-is since it's flag-guarded and is big enough for now, and we can follow up w/ fixes on another PR. I'm also going on vacation starting tomorrow until the 18th, so will resume looking at this when I get back.) |
interesting, ill see if i can write a test (in another PR!) to replicate this.
So the JVM avoids special handling, but tries a bit harder after 127 invocations (controlled by So i think the solution is one of....
|
This would be sad. :-(
Seems unfortunate but maybe necessary?
+1 bad tradeoff.
I think effectively suppressing the test (whether ignoring LambdaForms or reducing the threshold & expecting an exact # of things) depends on the consequences in real-life behaviors. We need to ensure that the long-lived LambdaForms created per-request (from the invokeExacts) aren't created per every request in a unique way per request. Maybe the problem is just that the warmup threshold is lower than the customize threshold, so its being inlined after the warmup, but we're not actually creating a new LambdaForm per request, we're just doing it "per unique signature used in practice" (which shouldn't really grow, IIUC). WDYT? |
This series addresses known issues in the experimental method handle implementation
Dependency
from thegetHandle
to be a runtime method parameter and changing InternalFactory to be an abstract classProvider
subclasses to holdinvokedynamic
instructions and instead use a simple hand written Provider class that lazily allocates aMethodHandle
and calls invokeExact.invokeExact
call.I imagine there are some as yet undiscovered issues but this should unblock broader experimentation.