Skip to content

Use MethodHandle in processing related to value class #1018

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

Merged
merged 12 commits into from
Jul 5, 2025

Conversation

k163377
Copy link
Contributor

@k163377 k163377 commented Jul 5, 2025

Background

To seamless handling of value classes with Jackson, reflection is heavily used in the related processing.
In many cases, multiple reflective calls are performed in sequence.

For example, during deserialization, at least two reflective calls are required: invoking the primary constructor and then a call to box.
Similarly, serialization typically requires at least two reflective calls — one to box and one to unbox.

Replacing these reflective operations with MethodHandles is expected to performance improvement.

Modification details

Each of the parts that used to use Method are replaced with MethodHandle.
In particular, the parts of the process in kotlin-module where Method was called continuously are further optimized by composing MethodHandle using MethodHandle.filterReturnValue.
In addition, for Int, Long, String, and (Java)UUID, which are particularly common types wrapped in value class, separate optimizations are made to speed up invocation by making the types explicit.

This change is based on the implementation in jackson-module-kogera 2.19.0-beta25.
https://github.com/ProjectMapK/jackson-module-kogera/releases/tag/2.19.0-beta25

It also fixes a problem where the unbox-impl method was not properly cached.
In particular, the problem of getting unbox-impl on every run in several cases related to serialization has been fixed, which will further greatly increase throughput in those cases.

Breaking change

Due to a change in execution, exceptions thrown by the constructor of value class are no longer wrapped in an InvocationTargetException.

Benchmark

The following repository was used for benchmarking:
k163377/jackson-value-class-benchmark

The commits to be compared are f54ef6e(before improvement) and d81fb82(after improvement).

Benchmark Details

The benchmark should compare performance with and without the aforementioned type-specific optimization.
Additionally, the benchmark was designed to minimize the overall processing done by Jackson itself, in order to better isolate and observe the impact of the MethodHandle based performance enhancements.

With these considerations in mind, the benchmarks were structured as follows:

  • Input/output JSON is kept minimal: single-digit integers and minimal JSON structure
  • Benchmark target types have only one property
  • Two value class types were tested: one wrapping an Int (with type-specific optimization), and one wrapping a Short (without type-specific optimization)

Since the benefits of this optimization increase with the number of value class properties involved, this benchmark serves as a lower bound for the performance improvements achievable in value class handling.

However, a preliminary check by kogera did not explicitly confirm the effect of type-specific optimizations in this benchmark because the percentage of processing related to value class was too low.
This is summarized in the following article(sorry, only in Japanese).
https://wrongwrong163377.hatenablog.com/entry/2025/07/05/175237

Also, kotlin-module has lower deserialization performance than kogera, so the amount of improvement seen from the benchmark is lower than that of kogera.

Throughput

A ratio greater than 1 indicates improvement.
summarized at: https://docs.google.com/spreadsheets/d/1Qi60pL8SEXJA5dx-CJA_NrON-OahTyb9RM5jDUhVk94

The overall trend is read as an improvement.

Serialize

In all cases, the scores showed a speedup in all cases.
In particular, the most common case, unbox.IntBenchmark.wrapped, showed more than a 2X speedup.

2.20.0-f54ef6e 2.20.0-d81fb82 2.20.0-d81fb82 / 2.20.0-f54ef6e
key.jsonKey.IntBenchmark.benchmark 1706154.900296 1721112.539959 1.008766871
key.jsonKey.ShortBenchmark.benchmark 1698948.371451 1753370.740705 1.032032974
key.unbox.IntBenchmark.benchmark 799204.108578 1516485.136584 1.89749417
key.unbox.ShortBenchmark.benchmark 548820.103790 844800.639366 1.539303377
jsonValue.IntBenchmark.direct 2492853.948175 2571880.673728 1.031701306
jsonValue.IntBenchmark.wrapped 1443995.099096 1575917.996655 1.091359657
jsonValue.ShortBenchmark.direct 2500459.150115 2570723.387372 1.028100534
jsonValue.ShortBenchmark.wrapped 1504655.607564 1608109.260028 1.068755702
unbox.IntBenchmark.direct 1334725.665322 2947865.011420 2.208592438
unbox.IntBenchmark.wrapped 879039.316159 1885195.465026 2.144608814
unbox.ShortBenchmark.direct 1212085.783568 2847329.922591 2.349115847
unbox.ShortBenchmark.wrapped 775937.494873 1669918.473655 2.152130145

Deserialize

In all cases, the scores showed a speedup in all cases.
In particular, the most common byPrimaryConstructor.IntBenchmark.wrapped case showed an improvement of about 3%.

2.20.0-f54ef6e 2.20.0-d81fb82 2.20.0-d81fb82 / 2.20.0-f54ef6e
KeyBenchmark._int 668132.908060 680694.531489 1.018801085
KeyBenchmark._short 542229.395140 574232.929115 1.05902213
byJsonCreator.IntBenchmark.direct 1453474.420878 1573540.213031 1.082606058
byJsonCreator.IntBenchmark.wrapped 651989.179429 681764.839236 1.045668948
byJsonCreator.ShortBenchmark.direct 1462410.784333 1582137.367836 1.081869325
byJsonCreator.ShortBenchmark.wrapped 663867.442290 700544.137815 1.055247016
byPrimaryConstructor.IntBenchmark.direct 1765387.321922 1823197.083530 1.03274622
byPrimaryConstructor.IntBenchmark.wrapped 702921.993057 721451.216566 1.026360284
byPrimaryConstructor.ShortBenchmark.direct 1541583.072049 1607385.550890 1.042685004
byPrimaryConstructor.ShortBenchmark.wrapped 661545.715857 685665.869853 1.036460298

Single Shot

A ratio less than 1 indicates improvement.
summarized at: https://docs.google.com/spreadsheets/d/1zAr0zH6AVeOGdWz6WJd5-42jz3FrK1aHR-AmIiVNaaY

Since the amount of initialization processing is increasing, the overall trend can be read as degradation.
However, the amount of degradation itself is at worst about 6%, and in many cases it is less than 3%.

Serialize

2.20.0-f54ef6e 2.20.0-d81fb82 2.20.0-d81fb82 / 2.20.0-f54ef6e
key.jsonKey.IntBenchmark.benchmark 184.196196 189.523739 1.028923198
key.jsonKey.ShortBenchmark.benchmark 185.018549 193.959258 1.048323312
key.unbox.IntBenchmark.benchmark 183.889096 186.475984 1.014067653
key.unbox.ShortBenchmark.benchmark 186.652562 189.655012 1.016085769
jsonValue.IntBenchmark.direct 169.141279 171.238232 1.012397642
jsonValue.IntBenchmark.wrapped 512.207452 507.520491 0.9908494869
jsonValue.ShortBenchmark.direct 165.888227 175.493462 1.057901849
jsonValue.ShortBenchmark.wrapped 498.692123 511.300525 1.025282938
unbox.IntBenchmark.direct 168.211594 171.327832 1.018525703
unbox.IntBenchmark.wrapped 498.314552 505.338075 1.014094557
unbox.ShortBenchmark.direct 180.360599 175.208861 0.9714364555
unbox.ShortBenchmark.wrapped 505.856824 504.002529 0.9963343482

Deserialize

2.20.0-f54ef6e 2.20.0-d81fb82 2.20.0-d81fb82 / 2.20.0-f54ef6e
KeyBenchmark._int 451.009276 454.054573 1.006752183
KeyBenchmark._short 449.179668 457.888895 1.019389183
byJsonCreator.IntBenchmark.direct 181.099398 177.060612 0.9776985123
byJsonCreator.IntBenchmark.wrapped 510.137011 515.377331 1.010272378
byJsonCreator.ShortBenchmark.direct 180.280929 179.065832 0.9932599804
byJsonCreator.ShortBenchmark.wrapped 517.763954 527.208727 1.018241465
byPrimaryConstructor.IntBenchmark.direct 441.188473 449.284595 1.018350711
byPrimaryConstructor.IntBenchmark.wrapped 514.552782 525.378941 1.021039939
byPrimaryConstructor.ShortBenchmark.direct 444.235007 453.849633 1.021643107
byPrimaryConstructor.ShortBenchmark.wrapped 516.214738 529.817651 1.026351268

k163377 added 11 commits July 5, 2025 23:30
Errors thrown by the constructor of value classes are no longer wrapped in InvocationTargetException.
Errors thrown by the constructor of value classes are no longer wrapped in InvocationTargetException.
@k163377 k163377 requested a review from Copilot July 5, 2025 15:33
Copy link

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR replaces reflective invocation with MethodHandle for value‐class (un)boxing to improve performance and consistency, updates serializers/deserializers to use these handles, and adjusts tests to expect IllegalArgumentException instead of InvocationTargetException.

  • Migrate caches and converters in ReflectionCache to use Class<*> keys and MethodHandle‐based converters.
  • Update serializers/deserializers (KotlinSerializers, KotlinKeySerializers, KotlinDeserializers, etc.) to use new handle‐based implementations.
  • Adjust tests in WithoutCustomDeserializeMethodTest.kt to expect IllegalArgumentException and compare it directly.

Reviewed Changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/test/kotlin/.../WithoutCustomDeserializeMethodTest.kt (mapKey) Updated assertThrows target and exception assertion to IllegalArgumentException.
src/test/kotlin/.../WithoutCustomDeserializeMethodTest.kt Same test update for standalone value class.
src/main/kotlin/.../ReflectionCache.kt Changed cache key types and added unbox/box converter caches.
src/main/kotlin/.../KotlinSerializers.kt Switched ValueClassSerializer to handle classes via MethodHandle.
src/main/kotlin/.../KotlinModule.kt Pass ReflectionCache into serializers/key‐serializers.
src/main/kotlin/.../KotlinKeySerializers.kt Refactored key serializers to use handle‐based implementation.
src/main/kotlin/.../KotlinKeyDeserializers.kt Introduced handle‐based key deserializers with specialized wrappers.
src/main/kotlin/.../KotlinDeserializers.kt Updated value‐class deserializers to use MethodHandle, split by conversion type.
src/main/kotlin/.../InternalCommons.kt Added shared MethodType constants and handle utilities.
src/main/kotlin/.../Converters.kt Rewrote converters to ValueClassBoxConverter/UnboxConverter with MethodHandle.
pom.xml Updated javadoc exclusions for new/renamed internal classes.
Comments suppressed due to low confidence (1)

src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt:41

  • The LRUMap is initialized with 0 as the initial capacity and reflectionCacheSize as the max, but originally both parameters were set to reflectionCacheSize. Using 0 may lead to immediate eviction or unexpected behavior. Consider LRUMap(reflectionCacheSize, reflectionCacheSize) to match its intended capacity.
        LRUMap(0, reflectionCacheSize)

@k163377 k163377 changed the title 【WIP】Use MethodHandle in processing related to value class Use MethodHandle in processing related to value class Jul 5, 2025
@k163377 k163377 marked this pull request as ready for review July 5, 2025 16:38
@k163377 k163377 merged commit d7f228e into FasterXML:2.x Jul 5, 2025
15 checks passed
@k163377 k163377 deleted the upgrade-value-class-perf branch July 5, 2025 17:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant