This PR replaces the Valhalla draft PR
https://github.com/openjdk/valhalla/pull/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.
<details>
<summary>Full results of <tt>valhalla.hash.FastPath</tt></summary>
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
</details>
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
---------
- [x] I confirm that I make this contribution in accordance with the [OpenJDK
Interim AI Policy](https://openjdk.org/legal/ai).
-------------
Commit messages:
- More review
- Comment
- Results
- Some cleanup
- Sort
- Comment
- More bench runs
- Fix order
- More
- Enable
- ... and 1 more: https://git.openjdk.org/jdk/compare/cc278dbb...21f03e61
Changes: https://git.openjdk.org/jdk/pull/32144/files
Webrev: https://webrevs.openjdk.org/?repo=jdk&pr=32144&range=00
Issue: https://bugs.openjdk.org/browse/JDK-8252185
Stats: 1707 lines in 22 files changed: 1646 ins; 23 del; 38 mod
Patch: https://git.openjdk.org/jdk/pull/32144.diff
Fetch: git fetch https://git.openjdk.org/jdk.git pull/32144/head:pull/32144
PR: https://git.openjdk.org/jdk/pull/32144