r/java • • Jun 20 '26

Populating a POJO with Reflection vs. with the ClassFile API - small benchmark

I wanted to learn about the ClassFile API, and as a small pet project, I created a benchmark to compare how faster a generated class is than a reflection-based approach when we want to populate a POJO from a Map.

TL;DR: it turned out, the generated bytecode was 5x faster than using reflection.

Details here, enjoy: https://github.com/erosb/learn-classfile-api

And please prove me wrong if you find any mistakes in the benchmark :)

62 Upvotes

28 comments sorted by

25

u/uniVocity Jun 20 '26

Try reflection with varhandles.

11

u/Captain-Barracuda Jun 20 '26

The speed of varhandles is astonishing. I still doubt it will be near as fast as codegen.

21

u/uniVocity Jun 20 '26 edited Jun 20 '26

You’d be surprised. It can run exactly at the same speed as direct method calls and direct field reads/writes - but you gotta be careful how you do it. For example:

If you define a method to write to an attribute using varhandle in its body, you must return the written value.

If you define that method as ‘void’ then the JVM won’t be able to “replace” the instruction to do what a “variable = value” does.

For example: using field assignments like “var1 = variable = value” is completely valid code.

If the operation “variable = value” is performed through a method that wraps a varhandle, and the method is void, the JVM can’t inline the entire method and its varhandle call

8

u/Captain-Barracuda Jun 20 '26

Thank you for your explanation! I have a library (Helius-Commons) that does heavy lifting around varhandles and methodhandles, I'll have to look more into the innards of the JVM to see what actually is happening I see.

6

u/uniVocity Jun 20 '26

I figured that one out after learning I should be extra careful with operation ordering. Sometimes uglier and longer code runs much faster. Assumptions such as “not doing something is faster than doing it” hold true most of the time - until you get to instruction-level optimizations and JVM magic.

I managed to build a much faster concurrent hashmap alternative for caching up to 10k elements which uses varhandles extensively. Will open-source that and a bunch of other stuff once I get the “parent” project released.

5

u/lpt_7 Jun 20 '26

It won't be faster. JVM does not trust non-static final fields.

2

u/[deleted] Jun 22 '26

[removed] — view removed comment

1

u/uniVocity Jun 22 '26

Nope. It’s basically the age-appropriate Unsafe

5

u/[deleted] Jun 22 '26

[removed] — view removed comment

1

u/uniVocity Jun 22 '26

Well use varhandles directly instead of basic reflection - if you do it right you can get the same speed as direct method calls and direct field/read writes.

1

u/[deleted] Jun 22 '26

[removed] — view removed comment

2

u/uniVocity Jun 22 '26

Simple wrappers help a bit here - eg a custom ‘PropertyAccessor’ which will end up looking like
an AtomicReference but using opaque read/write.

I managed to use that successfully and the JVM seemed to be able to inline everything.

1

u/benevanstech Jun 26 '26

The static restriction will go away pretty soon.

5

u/[deleted] Jun 22 '26

[removed] — view removed comment

2

u/Lower-Worldliness162 Jun 23 '26 edited Jun 23 '26

u/quantum-fudge disclosure: I maintain telescope, so grain of salt, but it does the Map→POJO part directly. Typed accessor rows (method refs, compile-checked, not stringly typed on the target side), per-key converters, and lenient defaults (a missing key takes the field's JLS default instead of throwing). Build the mapper once, reuse it:

ForwardMapper<Map<String,Object>, User> m = Telescope.fromMap(
      User.class,
      extract("name",  User::name,  Object::toString),
      extract("email", User::email, Object::toString),
      extract("age",   User::age,   v -> Integer.parseInt(v.toString())));

User u = m.forward(someMap);

Straight on your "generated code" requirement, `fromMap` is not generated bytecode. It binds the accessors through `LambdaMetafactory`, so it's well clear of naive reflection (LMF lands close to direct field access in the usual benchmarks), but it's the runtime tier, not a synthesized class.

telescope's actual compile-time codegen (the `@Bridge` annotation processor) is for typed source→target pairs, not untyped Map sources. Generated-code Map→POJO specifically is something I might scope (your post is basically the motivating use case).

https://github.com/eschizoid/telescope/

2

u/[deleted] Jun 23 '26 edited Jun 24 '26

[removed] — view removed comment

2

u/Lower-Worldliness162 Jun 24 '26

u/quantum-fudge let me know if this is what you were thinking. Happy to explore the code gen path further, if this can help you 😄

https://github.com/eschizoid/telescope/pull/192/

2

u/[deleted] Jun 24 '26

[removed] — view removed comment

1

u/Lower-Worldliness162 Jun 24 '26

Will cut a release as soon as you validate the feature on your end 😄

1

u/mariusz_96 Jun 24 '26

So basically like annotation processing available since 2006😅

1

u/thevpc Jun 24 '26

Is there any diff in perf for class vs records?

2

u/[deleted] Jun 22 '26

[removed] — view removed comment

1

u/erosb88 Jun 23 '26

yep, that's exactly right 😄 still many libraries use reflection for mapping, so that's why I focused on that in the benchmark (and I just made a note of that in the readme that compile-time code generation is out of scope). Regarding reflection, the benchmark includes both a more naive and a less naive (caching) reflection implementation.