Fold MemorySegment primitive get/set into direct accesses on JDK 21+#24304
Fold MemorySegment primitive get/set into direct accesses on JDK 21+#24304nbhuiyan wants to merge 1 commit into
Conversation
MemorySegment.get and MemorySegment.set methods for byte, char, short,
int, long, float and double data types dispatch through a deep VarHandle /
MethodHandle / LambdaForm / VarHandleSegmentAsX / ScopedMemoryAccess
chain. Even when such call chains inline fully, they leave a sequence of guards,
such as LambdaForm dispatch, segment accessors and session checks, each of
which cost a compare and branch per access and cannot be hoisted out of loops.
This commit implements direct lowering of these accessors in
J9RecognizedCallTransformer during its early pass (part of ILGen opts)
to a guarded direct load or store at segment.min + offset whenever the layout
argument is known at compile time. The per-access runtime overhead of the slow
chain is replaced by compile-time folding of the layout's byte order and
alignment constraint plus a single combined guard.
The guard ORs together every condition the interpreted path checks and
branches to the original call when any of them fails:
- receiver is NativeMemorySegmentImpl or MappedMemorySegmentImpl (heap
segments take the slow path)
- 0 <= offset <= length - accessSize (bounds check)
- min + offset aligned to the layout's byteAlignment, for
alignment-constrained layouts (folded away for *_UNALIGNED layouts)
- scope state is read through a volatile load, so the check is neither
hoisted nor commoned across iterations
- owner == null || owner == currentThread
- !readOnly (for stores)
The slow path is the unmodified original call, so each failing guard
preserves the exact semantics and throws exceptions as necessary.
Reversed-order layouts are handled by a compile-time endian conversion
(a byte swap on the value, float and double go through their integer bit
patterns).
MemorySegment.get/set are given fine-grained recognized method ids so the
transformation can match each primitive overload, and J9EstimateCodeSize keeps
treating them as MemorySegment methods so the InterpreterEmulator's stateful
bytecode iteration still folds accessHandle().
Two VM frontend queries, TR_J9VMBase::getLayoutByteOrder andgetLayoutByteAlignment,
that read the known layout object's order and byteAlignment have been implemented
for this transformation. Corresponding JITServer changes have also been implemented.
This transformation requires additional work for AOT support, so it is currently
skipped during AOT compilations. A new env option TR_disableFFMDirectLowering can
be used to disable the transformation.
Correctness of the fast path rests on three invariants:
* the scope-state load is volatile
* there is no yield point between that load and the access
* native segment memory is freed only after ScopedMemoryAccess.closeScope0 has taken
exclusive VM access
A racing close therefore either lets an in-flight access complete before the thread
gets to a yield point, or is observed as a closed state just prior the next
access, which results in fallback to the slow path. This also covers Cleaner-managed
implicit arenas without a reachabilityFence on the fast path.
Signed-off-by: Nazim Bhuiyan <nubhuiyan@ibm.com>
|
@vijaysun-omr @mpirvu @0xdaryl Requesting review from at least two of you. |
|
|
||
| // --- Compare tree: OR'd compound guard --- | ||
| TR::SymbolReference *vftSR = srTab->findOrCreateVftSymbolRef(); | ||
| TR::Node *segVft = TR::Node::createWithSymRef(node, TR::aloadi, 1, segmentNode->duplicateTree(), vftSR); |
There was a problem hiding this comment.
should we emit a test for null on the segmentNode value before we indirect ?
There was a problem hiding this comment.
I did not think that was necessary because we already had bounds checking earlier, binding segmentNode to <= requiredChildren. createTempsForCalls would also have iterated over the call node's children and dereferenced segmentNode child.
There was a problem hiding this comment.
sorry I meant a null test in generated code, not a null test at compile time
There was a problem hiding this comment.
Yes, a NULLCHK on the segmentNode is inserted just before the diamond. See Lines 634 onwards:
// NULLCHK on the segment before the diamond: it must precede the vft load
// in the guard, and the segment has to be non-null on the slow path too.
TR::SymbolReference *nullChkSR = srTab->findOrCreateNullCheckSymbolRef(comp()->getMethodSymbol());
TR::Node *passThrough = TR::Node::create(node, TR::PassThrough, 1, segmentNode->duplicateTree());
TR::Node *nullChk = TR::Node::createWithSymRef(node, TR::NULLCHK, 1, passThrough, nullChkSR);
treetop->insertBefore(TR::TreeTop::create(comp(), nullChk));There was a problem hiding this comment.
Thanks, I saw that after I posted my comment. I assume it is okay to raise an actual exception at this point since the original call would have raised such an exception (NPE) as well.
|
|
||
| // --- Fast tree: direct access at min + offset --- | ||
| TR::Node *minLoad = TR::Node::createWithSymRef(node, TR::lloadi, 1, segmentNode->duplicateTree(), minSR); | ||
| TR::Node *addr = TR::Node::create(node, TR::ladd, 2, minLoad, offsetNode->duplicateTree()); |
There was a problem hiding this comment.
duplicateTree can be a tricky api in that it duplicates the subtree to be evaluated at the new location, so if any nodes were anchored and the relevant symbols had their values changed, the calling it here could result in a new load being created instead of getting the anchored value, thus causing different semantics
There was a problem hiding this comment.
Maybe this is impossible here because there are temp loads created and they are known not to change in the interim
There was a problem hiding this comment.
I had to implement similar transformations in the past and used duplicateTree, where I got to learn (through trial and error) about the importance of first creating temps for call before duplicating. Is there something additional to be done to ensure we don't run into issues in the future?
There was a problem hiding this comment.
that is good, and probably sufficient if you can convince yourself that there will not be any commoning of a load across a store of the temp in question....i.e.
load x
...
store x
... rhs
...
==> load x
situation cannot happen.
|
Given the long sequence of tree transformations for the different API cases being handled, I think a question has to be asked about the kind of unit testing possible/done here. It is going to be hard to spot bugs in the sequence of transformations and we should probably make sure every API case is at least exercised. |
I have exercised all of these transformations through a microbenchmark I used to analyze performance. With the help of IBM Bob, I have also created a set of verification tests (derived from existing openjdk tests) that confirm that that guard-failure conditions are properly set up and we maintain the expected behaviour through fallback to the slow path. As for the existing coverage, all of the different data types are similar in semantics, so tests that cover a subset of the data types effectively cover all. We do have an extensive amount of coverage as part of the openjdk test targets (the sources for them located under |
|
jenkins test sanity.functional all jdk25 |
|
I would recommend running sanity.openjdk test as well, as that exercises the transformer. |
|
jenkins test sanity.openjdk all jdk25 |
|
jenkins test sanity.openjdk all jdk25 depends eclipse-omr/omr#8337 |
|
I have relaunched tests with the potential fix for #24336 included in the build to remove all the constrefs-related noise. In the latest set of build, I have not seen any const-refs related failures yet. However, I am seeing a new failure that is probably directly a result of the changes: sanity.openjdk aarch64 mac failure: I have not seen this failure in any of my testing, so this needs investigating. |
|
sanity.openjdk x86-64 Linux contains the symref-related failure seen in aarch64 mac, and contains an additional failure that requires investigating: All other failures I have seen so far on other platforms is an instance of these two distinct problems. |
MemorySegment.get and MemorySegment.set methods for byte, char, short, int, long, float and double data types dispatch through a deep VarHandle / MethodHandle / LambdaForm / VarHandleSegmentAsX / ScopedMemoryAccess chain. Even when such call chains inline fully, they leave a sequence of guards, such as LambdaForm dispatch, segment accessors and session checks, each of which cost a compare and branch per access and cannot be hoisted out of loops.
This commit implements direct lowering of these accessors in J9RecognizedCallTransformer during its early pass (part of ILGen opts) to a guarded direct load or store at segment.min + offset whenever the layout argument is known at compile time. The per-access runtime overhead of the slow chain is replaced by compile-time folding of the layout's byte order and alignment constraint plus a single combined guard.
The guard ORs together every condition the interpreted path checks and branches to the original call when any of them fails:
The slow path is the unmodified original call, so each failing guard preserves the exact semantics and throws exceptions as necessary. Reversed-order layouts are handled by a compile-time endian conversion (a byte swap on the value, float and double go through their integer bit patterns).
MemorySegment.get/set are given fine-grained recognized method ids so the transformation can match each primitive overload, and J9EstimateCodeSize keeps treating them as MemorySegment methods so the InterpreterEmulator's stateful bytecode iteration still folds accessHandle().
Two VM frontend queries, TR_J9VMBase::getLayoutByteOrder andgetLayoutByteAlignment, that read the known layout object's order and byteAlignment have been implemented for this transformation. Corresponding JITServer changes have also been implemented.
This transformation requires additional work for AOT support, so it is currently skipped during AOT compilations. A new env option TR_disableFFMDirectLowering can be used to disable the transformation.
Correctness of the fast path rests on three invariants:
This is the final part of the 3-part MemorySegment improvement work. Other two parts: