> On Jan 18, 2022, at 6:43 AM, Michael Kuhlmann <j...@fiolino.de> wrote:
> 
> Hi Steven!
> 
> Sorry for coming back so lately; the main reason is that I can't answer your 
> question. The issue you are describing indeed is a bit weird, and I can only 
> speculate that it's like this because LambdaMetaFactory was mainly designed 
> to be used in bootstrap methods for invokedynamic calls. So the checks are 
> probably more restrictive than it should be when being used in standard Java 
> code.

Thanks. I had a suspicion that this would be the answer :)

> But I'm curious about your motivation on using Lambdas at all. Of course, I 
> don't have detailed insights and may miss some important context. Maybe my 
> idea is completely wrong and nonsense; in that case I apologize for being so 
> intrusive. But I was already wondering in the past why Jackson is using 
> bytecode generation when simple MethodHandle combinations would be much 
> easier to use.
> 
> Lambdas like Function and BiConsumer seem to be a bad choice for me. There 
> are two main downsides to it:
> - LambdaMetaFactory only accepts DirectMethodHandles. At least it allows 
> simple type conversions as autoboxing and hierarchical castings, but that's 
> it. Even a simple conversion from a String input (what you probably have when 
> parsing a JSON structure) to a numeric value is impossible to integrate.

Right, the optimization I have written is not so complete as to build the full 
conversion stack as MethodHandles.

Essentially we are using it only to replace Method[Handle].invoke(), since at 
the time it was written both carried somewhat of a performance penalty:
https://www.optaplanner.org/blog/2018/01/09/JavaReflectionButMuchFaster.html

Jackson's core implementation reads a stream of tokens, and given a Bean-like 
object, will react to properties by parsing values and calling a setter.
The tokenizer already takes care of all conversions: there's a hand-rolled UTF8 
parser that will emit primitives or String values from the token stream.

This optimization only covers the "calling a setter" or "calling a getter" part 
of the process in response to tokens from the parser.
We do have specializations for common primitives (int, long, double) to avoid 
boxing.

So Jackson core will read a property name, deserialize e.g. a long or String or 
nested object, and then need to call a method on the object to store the 
(possibly primitive) value.

> - I assume you plan to iterate over the generated lambdas. In this case, 
> Hotspot won't be able to optimize much because each Function/BiConsumer has a 
> different implementation. It's impossible to predict much or even inline 
> much. I would expect the performance to be much lower than the current one.

Interestingly, that wasn't what I found.  At least as of Java 14, the 
Metafactory generation approach (as limited as my first pass was) was 
competitive with CGLib hand-generated code in many (but not all) cases, and 
significantly faster than Method.invoke:
https://github.com/FasterXML/jackson-modules-base/tree/2.14/blackbird/benchmarks
Unfortunately I don't have a MethodHandle.invokeExact comparison in that 
benchmark. I should go back and finish that work, and re-run it all on 17. If 
we can call non-static MethodHandles with little overhead, then any reason to 
use the Metafactory here goes away.

I did try to explore how the inlining worked out using jitwatch but I ran into 
problems: https://github.com/AdoptOpenJDK/jitwatch/issues/282  

> 
> Why not just concatenating MethodHandles directly? The strategy could be as 
> the following:
> - Use filterArguments to make the getter's return type and the setter's 
> argument convertable
> - Use filterArguments again to directly inject the getter into the setter's 
> argument; so getter and setter will merge into one "transfer" handle
> - Use foldArguments to consecutively perform the transfer handle for each 
> attribute; the resulting handle will transfer all attributes from source to 
> destination
> - Use foldArgument again with MethodHandles::identity to let this transfer 
> handle return its argument
> - And again, use foldArguments with the constructor handle to convert the 
> resulting handle into a simple one-argument-handle expecting the source as a 
> parameter and the newly creation target element as the return value.
> - Use such a handle also for type conversion of nested Pojos (for to-many 
> relations, you have to be creative).
> 
> As a result, you will have only one simple MethodHandle to convert the whole 
> structure at once.
> As a background, back in times I was writing a library to convert Solr result 
> objects to standard Pojos. The main logic was to convert Maps to nested Pojos 
> and vice versa. I used exactly that strategy. It as fun, much better than 
> solving Sudoku ;), and it worked well even with Java 8 features. With the new 
> loop and iterate handles introduced with Java 9, you have even more options.

Cool! Yes, all the new MethodHandle factories are quite interesting.

> Really, I'm curious if this could be an approach for Jackson. Or if not, what 
> could be the obstacles.

I think the problem here is just slightly different: your approach will copy 
from one Java object to another Java object, but what we are doing is reacting 
to a token stream and trying to bind it to Java objects with as little overhead 
as possible.

> 
> 
> On 1/12/22 9:56 PM, Steven Schlansker wrote:
>> Hi core-libs-dev,
>> I am maintaining a module for the popular Jackson JSON library that attempts 
>> to simplify code-generation code without losing performance.
>> Long ago, it was a huge win to code-generate custom getter / setter / field 
>> accessors rather than use core reflection. Now, the gap is closing a lot 
>> with MethodHandles, but there still seems to be some benefit.
>> The previous approach used for code generation relied on the CGLib + ASM 
>> libraries, which as I am sure you know leads to horrible-to-maintain code 
>> since you essentially write bytecode directly.
>> Feature development basically stopped because writing out long chains of 
>> `visitVarInsn(ASTORE, 3)` and the like scares off most contributors, myself 
>> included.
>> As an experiment, I started to port the custom class generation logic to use 
>> LambdaMetafactory. The idea is to use the factory to generate 
>> `Function<Bean, T>` getter and `BiConsumer<Bean, T>` setter implementations.
>> Then, use those during (de)serialization to access or set data.  Eventually 
>> hopefully the JVM will inline, removing all (?) reflection overhead.
>> The invocation looks like:
>> var lookup = MethodHandles.privateLookupIn(targetClass, 
>> MethodHandles.lookup()); // allow non-public access
>> var getter = lookup.unreflect(someGetterMethod);
>> LambdaMetafactory.metafactory(
>>   lookup,
>>   "apply",
>>   methodType(Function.class),
>>   methodType(Object.class, Object.class),
>>   getter,
>>   getter.type())
>> This works well for classes from the same classloader. However, once you try 
>> to generate lambdas with implementations loaded from a different 
>> classloader, you run into a check in the AbstractValidatingLambdaMetafactory 
>> constructor:
>> if (!caller.hasFullPrivilegeAccess()) {
>>   throw new LambdaConversionException(String.format(
>>     "Invalid caller: %s",
>>     caller.lookupClass().getName()));
>> }
>> The `privateLookupIn` call seems to drop MODULE privilege access when 
>> looking across ClassLoaders. This appears to be because the "unnamed module" 
>> differs between a ClassLoader and its child.
>> This happens without the use of modulepath at all, only classpath, where I 
>> would not expect module restrictions to be in play.
>> Through some experimentation, I discovered that while I cannot call the 
>> LambdaMetafactory with this less-privileged lookup, I am still allowed to 
>> call defineClass.
>> So, I compile a simple class:
>> package <targetclasspackage>;
>> class AccessCracker { static final Lookup LOOKUP = MethodHandles.lookup(); }
>> and inject it into the target class's existing package:
>> lookup = lookup.defineClass(compiledBytes).getField("LOOKUP").get(null);
>> and now I have a full privileged lookup into the target classloader, and the 
>> Metafactory then seems to generate lambdas without complaint.
>> This workaround seems to work well, although it's a bummer to have to 
>> generate and inject these dynamic accessor classes.
>> It feels inconsistent that on one hand my Lookup is not powerful enough to 
>> generate a simple call-site with the Metafactory,
>> but at the same time it is so powerful that I can load arbitrary bytecode 
>> into the target classloader, and thus indirectly do what I wanted to do in 
>> the first place (with a fair bit more work)
>> There's a bit of additional context here:
>> https://github.com/FasterXML/jackson-modules-base/issues/138
>> https://github.com/FasterXML/jackson-modules-base/pull/162/files
>> Any chance the Metafactory might become powerful enough to generate call 
>> sites even across such unnamed Modules in a future release? Or even more 
>> generally across arbitrary Modules, if relevant access checks pass?
>> I'm also curious for any feedback on the overall approach of using the 
>> Metafactory, perhaps I am way off in the weeds, and should just trust 
>> MethodHandles to perform well if you use invokeExact :) JMH does seem to 
>> show some benefit though especially with Graal compiler.
>> Thanks a bunch for any thoughts,
>> Steven Schlansker

Reply via email to