Skip to content

8252185: [lworld] Improve performance of identityHashCode for value objects - #32144

Open
marc-chevalier wants to merge 13 commits into
openjdk:masterfrom
marc-chevalier:JDK-8252185
Open

8252185: [lworld] Improve performance of identityHashCode for value objects#32144
marc-chevalier wants to merge 13 commits into
openjdk:masterfrom
marc-chevalier:JDK-8252185

Conversation

@marc-chevalier

@marc-chevalier marc-chevalier commented Jul 31, 2026

Copy link
Copy Markdown
Member

This PR replaces the Valhalla draft PR openjdk/valhalla#2642, which was not integrated due to the code freeze immediately before the Valhalla mainline integration.


It is conceptually very similar to acmp: it has two parts.

Static Expansion

If the operand of identityHashCode is known at compile-time, we can basically inline the implementation of ValueObjectMethods.valueObjectHashCode. There are a few points worth noting.

  1. The seed of the hash is the hash of the mirror class object. That object is not a value object, but an identity class. We look in the header of the said class whether it was cached already, which is very likely. Otherwise, we give up the static expansion: it is not worth replacing a call with a call.
  2. Oops are hell. We don't expand when oops are involved.
  3. The runtime implementation performs some unsafe gets to get all pieces of a segment in a simple, greedy way. Which means that in the case we have a value class made of 2 int, the likely outcome is that it would get both at once with a single call to getLong. This is fine, we can do that, but we need to mark the access as mismatch and unsafe (but aligned, since the acmp maps are smartly done). But in the case we have a class made of a single int, it would be unfortunate to mark the getInt access as mismatch since it prevents some optimizations. So if we are getting exactly a field, we detect it, and we mark the load as non-mismatch (match?).

Fast Path

This supports only objects with a simple shape: one segment of data, being 1, 2, 4 or 8-byte long, which is enough to cover many migrated classes (but not dates for instance). This feature is morally very similar to the acmp fast path. It is controlled by the diagnostic flag UseHashcodeFastPath. Alike acmp, we need to sabotage the fast path in case we can do a static expansion later.

I can only suggest you take a look at inlineKlass.hpp on how that works.

Benchmarking

Microbenchmarking is rather unsurprising:

  • haven't changed:
    • null was and is still fast (1-2ns)
    • hashcode cached in the header was and is still fast (2ns)
    • identity objects without cached hashcode are not too slow, and still aren't (20-25ns)
    • known at compile time (60-70ns):
      • value objects with oops
    • unknown at compile time (60-70ns):
      • value objects with sizes that are not 1, 2, 4 or 8
      • value objects with oops
  • got better (60-70ns -> 2-7ns):
    • value objects with a type known at compile time, without oops
    • value objects with a type unknown at compile time, without oops, of size 1, 2, 4 or 8.
Full results of valhalla.hash.FastPath

Before:

Benchmark                                      Mode  Cnt   Score   Error  Units
FastPath.big_mix                               avgt   15  49.680 ± 0.628  ns/op
FastPath.heterogeneous_small                   avgt   15  48.337 ± 0.865  ns/op
FastPath.heterogeneous_too_big                 avgt   15  57.891 ± 0.652  ns/op
FastPath.heterogeneous_with_obj                avgt   15  22.411 ± 0.323  ns/op
FastPath.homogeneous_byte                      avgt   15  56.707 ± 1.043  ns/op
FastPath.homogeneous_byte_static               avgt   15  57.202 ± 1.102  ns/op
FastPath.homogeneous_empty                     avgt   15  56.501 ± 1.734  ns/op
FastPath.homogeneous_empty_static              avgt   15  54.758 ± 1.700  ns/op
FastPath.homogeneous_int                       avgt   15  56.281 ± 1.197  ns/op
FastPath.homogeneous_int_int                   avgt   15  57.415 ± 1.268  ns/op
FastPath.homogeneous_int_int_static            avgt   15  56.711 ± 1.197  ns/op
FastPath.homogeneous_int_static                avgt   15  57.024 ± 1.262  ns/op
FastPath.homogeneous_long                      avgt   15  57.192 ± 1.170  ns/op
FastPath.homogeneous_long_static               avgt   15  56.798 ± 1.375  ns/op
FastPath.homogeneous_null                      avgt   15   1.371 ± 0.040  ns/op
FastPath.homogeneous_short                     avgt   15  58.196 ± 1.259  ns/op
FastPath.homogeneous_short_static              avgt   15  58.724 ± 1.449  ns/op
FastPath.homogeneous_too_big_long_int          avgt   15  61.896 ± 2.272  ns/op
FastPath.homogeneous_too_big_long_int_static   avgt   15  59.864 ± 1.394  ns/op
FastPath.homogeneous_too_big_long_long         avgt   15  59.559 ± 1.188  ns/op
FastPath.homogeneous_too_big_long_long_static  avgt   15  59.124 ± 1.078  ns/op
FastPath.homogeneous_weird_size                avgt   15  60.049 ± 1.678  ns/op
FastPath.homogeneous_weird_size_static         avgt   15  59.820 ± 1.373  ns/op
FastPath.homogeneous_with_obj_array            avgt   15  22.113 ± 0.757  ns/op
FastPath.homogeneous_with_obj_string           avgt   15  23.866 ± 0.444  ns/op
FastPath.homogeneous_with_oop                  avgt   15  71.719 ± 1.863  ns/op
FastPath.homogeneous_with_oop_static           avgt   15  71.977 ± 1.539  ns/op
FastPath.pre_hashed                            avgt   15   1.906 ± 0.054  ns/op

After:

Benchmark                                      Mode  Cnt   Score   Error  Units
FastPath.big_mix                               avgt   15  27.101 ± 0.519  ns/op
FastPath.heterogeneous_small                   avgt   15   5.175 ± 0.104  ns/op
FastPath.heterogeneous_too_big                 avgt   15  62.371 ± 1.041  ns/op
FastPath.heterogeneous_with_obj                avgt   15  23.229 ± 0.408  ns/op
FastPath.homogeneous_byte                      avgt   15   5.878 ± 0.071  ns/op
FastPath.homogeneous_byte_static               avgt   15   2.407 ± 0.054  ns/op
FastPath.homogeneous_empty                     avgt   15   3.459 ± 0.082  ns/op
FastPath.homogeneous_empty_static              avgt   15   2.156 ± 0.047  ns/op
FastPath.homogeneous_int                       avgt   15   5.916 ± 0.044  ns/op
FastPath.homogeneous_int_int                   avgt   15   7.530 ± 0.076  ns/op
FastPath.homogeneous_int_int_static            avgt   15   7.120 ± 0.109  ns/op
FastPath.homogeneous_int_static                avgt   15   2.117 ± 0.035  ns/op
FastPath.homogeneous_long                      avgt   15   3.668 ± 0.072  ns/op
FastPath.homogeneous_long_static               avgt   15   2.211 ± 0.055  ns/op
FastPath.homogeneous_null                      avgt   15   1.415 ± 0.042  ns/op
FastPath.homogeneous_short                     avgt   15   5.832 ± 0.058  ns/op
FastPath.homogeneous_short_static              avgt   15   2.099 ± 0.025  ns/op
FastPath.homogeneous_too_big_long_int          avgt   15  62.184 ± 1.775  ns/op
FastPath.homogeneous_too_big_long_int_static   avgt   15   2.536 ± 0.033  ns/op
FastPath.homogeneous_too_big_long_long         avgt   15  60.679 ± 1.968  ns/op
FastPath.homogeneous_too_big_long_long_static  avgt   15   2.894 ± 0.103  ns/op
FastPath.homogeneous_weird_size                avgt   15  60.444 ± 1.753  ns/op
FastPath.homogeneous_weird_size_static         avgt   15   2.410 ± 0.039  ns/op
FastPath.homogeneous_with_obj_array            avgt   15  22.019 ± 0.359  ns/op
FastPath.homogeneous_with_obj_string           avgt   15  24.011 ± 0.332  ns/op
FastPath.homogeneous_with_oop                  avgt   15  73.037 ± 1.287  ns/op
FastPath.homogeneous_with_oop_static           avgt   15  74.006 ± 2.115  ns/op
FastPath.pre_hashed                            avgt   15   1.888 ± 0.063  ns/op

On real benchmark, improvements are noticeable only on hash-intensive cases.

Future Work

We can also add profiling and speculate on the type of the argument, as we do for acmp. That is left as an exercise for the reader.

The current implementation of both the static expansion and the fast path are not caching the result in the header. There are a few reasons for that:

  1. It is not clear it is worth it.
  2. It is non-trivial to do only when there is already a buffer, to avoid keeping an allocation around just for that. This will be easier in the future, with a macro-based hashcode, where the expansion would do the storing only if something else kept the buffer alive (or already exists). With that, we also would need not to remove the fast path after static expansion.
  3. It is non-trivial to store something in the header while being race-safe. It probably needs cooperation of the backend to do something alike JVM_IHashCode.

Thanks,
Marc



Progress

  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue
  • Change must be properly reviewed (2 reviews required, with at least 1 Reviewer, 1 Author)

Issue

  • JDK-8252185: [lworld] Improve performance of identityHashCode for value objects (Enhancement - P2)

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/32144/head:pull/32144
$ git checkout pull/32144

Update a local copy of the PR:
$ git checkout pull/32144
$ git pull https://git.openjdk.org/jdk.git pull/32144/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 32144

View PR using the GUI difftool:
$ git pr show -t 32144

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/32144.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper

bridgekeeper Bot commented Jul 31, 2026

Copy link
Copy Markdown

👋 Welcome back mchevalier! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk

openjdk Bot commented Jul 31, 2026

Copy link
Copy Markdown

❗ This change is not yet ready to be integrated.
See the Progress checklist in the description for automated requirements.

@openjdk openjdk Bot added hotspot hotspot-dev@openjdk.org core-libs core-libs-dev@openjdk.org labels Jul 31, 2026
@openjdk

openjdk Bot commented Jul 31, 2026

Copy link
Copy Markdown

@marc-chevalier The following labels will be automatically applied to this pull request:

  • core-libs
  • hotspot

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing lists. If you would like to change these labels, use the /label pull request command.

@openjdk

openjdk Bot commented Jul 31, 2026

Copy link
Copy Markdown

The total number of required reviews for this PR has been set to 2 based on the presence of this label: hotspot. This can be overridden with the /reviewers command.

@marc-chevalier
marc-chevalier marked this pull request as ready for review July 31, 2026 15:38
@openjdk openjdk Bot added the rfr Pull request is ready for review label Jul 31, 2026
@mlbridge

mlbridge Bot commented Jul 31, 2026

Copy link
Copy Markdown

Webrevs

@TobiHartmann TobiHartmann left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Great work Marc! I did a first pass and added a few comments. Will take another look tomorrow with a fresh brain 😄

Comment thread src/hotspot/share/opto/library_call.cpp Outdated
Comment thread test/hotspot/jtreg/compiler/valhalla/inlinetypes/TestHashcodeFastPath.java Outdated
public void run2() {
int SIZE = 100;
for (int i = 0; i < SIZE; ++i) {
Asserts.assertEQ(h_byte(new Byte((byte)i)), h(new Byte((byte)i)));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Negative values are missing here.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Added, but I now get a test failure (line 484) during warmup. PrintCompilation confirms nothing relevant is compiled yet... I'll look at that, but it's very puzzling.

Comment thread src/hotspot/share/opto/library_call.cpp Outdated
Comment thread src/hotspot/share/opto/inlinetypenode.cpp Outdated
Comment thread src/hotspot/share/opto/library_call.cpp Outdated
Comment thread src/hotspot/share/opto/inlinetypenode.cpp Outdated
Comment thread test/hotspot/jtreg/compiler/valhalla/inlinetypes/TestHashcodeFastPath.java Outdated
Comment thread src/hotspot/share/opto/inlinetypenode.hpp Outdated
IfNode* iff_is_long_payload = create_and_map_if(control(), is_long_payload_bol, PROB_FAIR, COUNT_UNKNOWN);

// Case 2. one segment, less than 8-byte long
Node* result_int = AddI(MulI(intcon(31), result_empty), ConvL2I(obj_extracted));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm not convinced that this is correct for signed byte and short. Isn't ConvL2I omitting the sign bit?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, something is off, but I can't make it fail. Not sure why not.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core-libs core-libs-dev@openjdk.org hotspot hotspot-dev@openjdk.org rfr Pull request is ready for review

Development

Successfully merging this pull request may close these issues.

2 participants