Skip to content

Commit e176dc4

Browse files
kittyoraclewoess
authored andcommitted
[GR-59627] Aggressively hoist trusted final fields
PullRequest: graal/20821
2 parents 53cd15f + a79c0d4 commit e176dc4

File tree

13 files changed

+342
-74
lines changed

13 files changed

+342
-74
lines changed

compiler/src/jdk.graal.compiler.test/src/jdk/graal/compiler/truffle/test/FrameHostReadsTest.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
package jdk.graal.compiler.truffle.test;
2626

2727
import java.io.IOException;
28+
import java.lang.invoke.VarHandle;
29+
import java.util.Objects;
2830
import java.util.function.Consumer;
2931

3032
import org.graalvm.word.LocationIdentity;
@@ -33,11 +35,14 @@
3335

3436
import com.oracle.truffle.api.Truffle;
3537
import com.oracle.truffle.api.frame.FrameDescriptor;
38+
import com.oracle.truffle.api.frame.FrameSlotKind;
3639
import com.oracle.truffle.api.impl.FrameWithoutBoxing;
3740
import com.oracle.truffle.api.test.SubprocessTestUtils;
3841

3942
import jdk.graal.compiler.api.directives.GraalDirectives;
43+
import jdk.graal.compiler.debug.GraalError;
4044
import jdk.graal.compiler.graph.Node;
45+
import jdk.graal.compiler.nodeinfo.Verbosity;
4146
import jdk.graal.compiler.nodes.FieldLocationIdentity;
4247
import jdk.graal.compiler.nodes.NamedLocationIdentity;
4348
import jdk.graal.compiler.nodes.StructuredGraph;
@@ -82,6 +87,9 @@ public void testStaticReads() throws IOException, InterruptedException {
8287
LocationIdentity identity = read.getLocationIdentity();
8388
if (identity instanceof FieldLocationIdentity) {
8489
fieldReads++;
90+
if (fieldReads > 1) {
91+
GraalError.guarantee(false, "%s", read.toString(Verbosity.All));
92+
}
8593
} else if (NamedLocationIdentity.isArrayLocation(identity)) {
8694
arrayReads++;
8795
} else if (identity == NamedLocationIdentity.ARRAY_LENGTH_LOCATION) {
@@ -141,6 +149,34 @@ public void testReadsWritesWithoutZeroExtend() throws IOException, InterruptedEx
141149
}).disableAssertions(FrameWithoutBoxing.class).postfixVmOption("-Djdk.graal.TruffleTrustedFinalFrameFields=true").run();
142150
}
143151

152+
static FrameWithoutBoxing escape;
153+
static final Object[] EMPTY_ARRAY = new Object[0];
154+
155+
/**
156+
* Test that the loads should not be hoisted incorrectly.
157+
*/
158+
public static Object snippetNoMoveReads(FrameDescriptor desc, Object obj) {
159+
FrameWithoutBoxing frame = new FrameWithoutBoxing(desc, EMPTY_ARRAY);
160+
frame.setObject(0, obj);
161+
VarHandle.fullFence();
162+
// Don't let the frame be scalar replaced
163+
escape = frame;
164+
VarHandle.fullFence();
165+
return frame.getObject(0);
166+
}
167+
168+
@Test
169+
public void testNoMoveReads() throws IOException, InterruptedException {
170+
SubprocessTestUtils.newBuilder(FrameHostReadsTest.class, () -> {
171+
FrameDescriptor.Builder descBuilder = FrameDescriptor.newBuilder();
172+
descBuilder.addSlot(FrameSlotKind.Object, null, null);
173+
FrameDescriptor desc = descBuilder.build();
174+
Object obj = new Object();
175+
Result res = test("snippetNoMoveReads", desc, obj);
176+
Assert.assertSame(Objects.toString(res.returnValue), obj, res.returnValue);
177+
}).disableAssertions(FrameWithoutBoxing.class).postfixVmOption("-Djdk.graal.TruffleTrustedFinalFrameFields=true").run();
178+
}
179+
144180
private void compileAndCheck(String snippet, Consumer<StructuredGraph> check) {
145181
getTruffleCompiler();
146182
initAssertionError();

compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/common/spi/ConstantFieldProvider.java

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2016, 2025, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -24,8 +24,14 @@
2424
*/
2525
package jdk.graal.compiler.core.common.spi;
2626

27-
import jdk.graal.compiler.options.OptionValues;
27+
import java.lang.foreign.MemorySegment;
2828

29+
import jdk.graal.compiler.core.common.util.PhasePlan;
30+
import jdk.graal.compiler.nodes.Invoke;
31+
import jdk.graal.compiler.nodes.StartNode;
32+
import jdk.graal.compiler.nodes.memory.FloatingReadNode;
33+
import jdk.graal.compiler.nodes.spi.CanonicalizerTool;
34+
import jdk.graal.compiler.options.OptionValues;
2935
import jdk.vm.ci.meta.JavaConstant;
3036
import jdk.vm.ci.meta.ResolvedJavaField;
3137

@@ -68,4 +74,70 @@ interface ConstantFieldTool<T> {
6874
default boolean maybeFinal(@SuppressWarnings("unused") ResolvedJavaField field) {
6975
return false;
7076
}
77+
78+
/**
79+
* Returns {@code true} if {@code field} is final and considered "trusted". A trusted final
80+
* field is immutable after the initialization of the holder object. This allows us to schedule
81+
* a load from {@code field} freely up until the point at which we can guarantee that the object
82+
* is initialized. It also allows us to assume that there is no store to the field after it is
83+
* read, skipping the anti-dependency calculation while scheduling.
84+
* <p>
85+
* Example 1:
86+
* {@snippet :
87+
* class Holder {
88+
* final int x;
89+
* }
90+
*
91+
* void test(Holder h) {
92+
* for (int i = 0; i < 100; i++) {
93+
* nonInlinedCall(h.x);
94+
* }
95+
* }
96+
* }
97+
* Without further contexts, we cannot assume that {@code nonInlineCall} will not modify
98+
* {@code h.x}, even if it is a final field, because {@code Unsafe} and the core reflection API
99+
* can be used to modify a final field even after initialization. Additionally, there's a risk
100+
* that a constructor of {@code Holder} may use {@code Holder.x} as a flag for inter-thread
101+
* communication. As a result, the load from {@code h.x} needs to be executed in each iteration.
102+
* However, if we can trust that {@code h} should be initialized before being passed to
103+
* {@code test} and {@code x} should not be modified post-initialization, then we can hoist the
104+
* load outside the loop.
105+
* <p>
106+
* When a {@link FloatingReadNode} reads from a field for which this method returns
107+
* {@code true}, we rewire its memory input to the earliest location possible in the CFG. In the
108+
* example above, we rewire the memory input of the load {@code h.x} to the {@link StartNode} of
109+
* the method {@code test}. This allows the load to have the maximum freedom in scheduling, as
110+
* well as enables all the loads from {@code h.x} to be GVN-ed by making their memory inputs
111+
* identical. Note that the load is still not immutable, this is important because if the method
112+
* {@code test} is later inlined into another graph, it may turn out that {@code h} is created
113+
* and {@code h.x} is assigned in the caller. In those cases, marking the load node as immutable
114+
* would be incorrect. On the other hand, since the memory input of the node is the
115+
* {@link StartNode} of the method {@code test}, after inlining, this node is expanded into the
116+
* memory node corresponding to the input memory state of the invocation to {@code test} in the
117+
* caller graph, we can still ensure that the load is not scheduled too freely which can bypass
118+
* the initialization in the caller. This is generally not a concern because
119+
* {@link FloatingReadNode}s are only introduced after the high tier, while inlining is only
120+
* performed during high tier. However, there may be other mechanisms such as snippets which can
121+
* inline code in a limited manner after high tier, and it is more future-proof to not rely on
122+
* the {@link PhasePlan} for the correctness of this transformation.
123+
* <p>
124+
* To ensure that the load has the maximum freedom in scheduling, we mark that the load has no
125+
* anti-dependency. In other word, it aliases with no store that is dominated by its memory
126+
* input, and we do not have to take anti-dependency into consideration while scheduling the
127+
* load. This is necessary both in terms of correctness and efficiency. As moving the memory
128+
* input may introduce incorrect anti-dependency with nodes between the old and the new memory
129+
* input in the CFG, potentially leading to an unschedulable graph.
130+
* <p>
131+
* This transformation can be performed if the holder object is:
132+
* <ul>
133+
* <li>A parameter that is not the receiver of a constructor, the earliest memory input can be
134+
* the {@link StartNode} of the graph.
135+
* <li>A value returned from a method invocation, the earliest memory input can be the
136+
* {@link Invoke} corresponding to the invocation (to do).
137+
* </ul>
138+
* It can be trivially extended to other trusted final fields in the JDK (e.g.
139+
* {@link MemorySegment}). However, extending it to general uses would risk violating the JVM
140+
* semantics.
141+
*/
142+
boolean isTrustedFinal(CanonicalizerTool tool, ResolvedJavaField field);
71143
}

compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/core/common/spi/JavaConstantFieldProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.util.Arrays;
2828

2929
import jdk.graal.compiler.debug.GraalError;
30+
import jdk.graal.compiler.nodes.spi.CanonicalizerTool;
3031
import jdk.graal.compiler.options.Option;
3132
import jdk.graal.compiler.options.OptionKey;
3233
import jdk.vm.ci.meta.JavaConstant;
@@ -197,4 +198,9 @@ protected boolean isWellKnownImplicitStableField(ResolvedJavaField field) {
197198
}
198199
return field.equals(stringValueField);
199200
}
201+
202+
@Override
203+
public boolean isTrustedFinal(CanonicalizerTool tool, ResolvedJavaField field) {
204+
return false;
205+
}
200206
}

compiler/src/jdk.graal.compiler/src/jdk/graal/compiler/nodes/java/LoadFieldNode.java

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,21 +77,29 @@ public final class LoadFieldNode extends AccessFieldNode implements Canonicaliza
7777

7878
private final Stamp uncheckedStamp;
7979

80+
/**
81+
* Records the injection of the property that this is a load from a trusted final field.
82+
*
83+
* @see ConstantFieldProvider#isTrustedFinal(CanonicalizerTool, ResolvedJavaField)
84+
*/
85+
private final boolean trustInjected;
86+
8087
protected LoadFieldNode(StampPair stamp, ValueNode object, ResolvedJavaField field, boolean immutable) {
81-
this(stamp, object, field, MemoryOrderMode.getMemoryOrder(field), immutable);
88+
this(stamp, object, field, MemoryOrderMode.getMemoryOrder(field), immutable, false);
8289
}
8390

84-
protected LoadFieldNode(StampPair stamp, ValueNode object, ResolvedJavaField field, MemoryOrderMode memoryOrder, boolean immutable) {
91+
protected LoadFieldNode(StampPair stamp, ValueNode object, ResolvedJavaField field, MemoryOrderMode memoryOrder, boolean immutable, boolean trustInjected) {
8592
super(TYPE, stamp.getTrustedStamp(), object, field, memoryOrder, immutable);
8693
this.uncheckedStamp = stamp.getUncheckedStamp();
94+
this.trustInjected = trustInjected;
8795
}
8896

8997
public static LoadFieldNode create(Assumptions assumptions, ValueNode object, ResolvedJavaField field) {
9098
return create(assumptions, object, field, MemoryOrderMode.getMemoryOrder(field));
9199
}
92100

93101
public static LoadFieldNode create(Assumptions assumptions, ValueNode object, ResolvedJavaField field, MemoryOrderMode memoryOrder) {
94-
return new LoadFieldNode(StampFactory.forDeclaredType(assumptions, field.getType(), false), object, field, memoryOrder, false);
102+
return new LoadFieldNode(StampFactory.forDeclaredType(assumptions, field.getType(), false), object, field, memoryOrder, false, false);
95103
}
96104

97105
public static ValueNode create(ConstantFieldProvider constantFields, ConstantReflectionProvider constantReflection, MetaAccessProvider metaAccess,
@@ -113,6 +121,20 @@ public static ValueNode createOverrideStamp(ConstantFieldProvider constantFields
113121
return canonical(null, stamp, object, field, constantFields, constantReflection, options, metaAccess, canonicalizeReads, allUsagesAvailable, false, position);
114122
}
115123

124+
/**
125+
* Returns a node that is the same as this node but is injected with the property that it is a
126+
* load from a trusted final field. May return this node if the injection is unnecessary or has
127+
* already been done.
128+
*
129+
* @see ConstantFieldProvider#isTrustedFinal(CanonicalizerTool, ResolvedJavaField)
130+
*/
131+
public LoadFieldNode withTrustInjected() {
132+
if (getLocationIdentity().isImmutable() || trustInjected) {
133+
return this;
134+
}
135+
return new LoadFieldNode(StampPair.create(stamp, uncheckedStamp), object(), field(), getMemoryOrder(), false, true);
136+
}
137+
116138
@Override
117139
public LocationIdentity getKilledLocationIdentity() {
118140
if (ordersMemoryAccesses()) {
@@ -237,4 +259,14 @@ public NodeCycles estimatedNodeCycles() {
237259
}
238260
return super.estimatedNodeCycles();
239261
}
262+
263+
/**
264+
* Returns {@code true} if this node is injected with the property that it is a load from a
265+
* trusted final field.
266+
*
267+
* @see ConstantFieldProvider#isTrustedFinal(CanonicalizerTool, ResolvedJavaField)
268+
*/
269+
public boolean trustInjected() {
270+
return trustInjected;
271+
}
240272
}

0 commit comments

Comments
 (0)