Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/*
* Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
*
* This code is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 2 only, as
* published by the Free Software Foundation. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the LICENSE file that accompanied this code.
*
* This code is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
* version 2 for more details (a copy is included in the LICENSE file that
* accompanied this code).
*
* You should have received a copy of the GNU General Public License version
* 2 along with this work; if not, write to the Free Software Foundation,
* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
*
* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
* or visit www.oracle.com if you need additional information or have any
* questions.
*/
package jdk.graal.compiler.core.test;

import java.util.Optional;

import org.graalvm.word.LocationIdentity;
import org.junit.Assert;
import org.junit.Test;

import jdk.graal.compiler.nodes.GraphState;
import jdk.graal.compiler.nodes.Invoke;
import jdk.graal.compiler.nodes.InvokeNode;
import jdk.graal.compiler.nodes.StructuredGraph;
import jdk.graal.compiler.nodes.memory.MemoryKill;
import jdk.graal.compiler.nodes.memory.WriteNode;
import jdk.graal.compiler.options.OptionValues;
import jdk.graal.compiler.phases.BasePhase;
import jdk.graal.compiler.phases.tiers.HighTierContext;
import jdk.graal.compiler.phases.tiers.Suites;

/** Tests dead-store elimination across invokes with precise memory effects. */
public class DeadStoreAcrossInvokeTest extends GraalCompilerTest {

private static final String UNRELATED_CALLEE_NAME = "unrelatedWork";
private static final String OBSERVING_CALLEE_NAME = "observingWork";
private static final String THROWING_CALLEE_NAME = "potentiallyThrowingWork";
private static final String ELIMINATED_SNIPPET_NAME = "eliminatedSnippet";

/** Value modified before and after the call. */
static final class Counter {
int value;
}

/** Value modified only by the call. */
static final class Logger {
int count;
}

/** Call modeled as non-throwing and accessing no relevant memory. */
@BytecodeParserNeverInline
static void unrelatedWork(Logger logger) {
if (logger != null) {
logger.count++;
}
}

/** Call modeled as non-throwing but observing the overwritten location. */
@BytecodeParserNeverInline
static void observingWork(Counter counter, Logger logger) {
if (logger != null) {
logger.count += counter.value;
}
}

/** Call modeled as potentially throwing before the overwriting store. */
@BytecodeParserNeverInline
static void potentiallyThrowingWork(Logger logger) {
logger.count++;
}

/** Contains a store overwritten after a call with disjoint effects. */
public static int eliminatedSnippet(Counter counter, Logger logger) {
counter.value = 1;
unrelatedWork(logger);
counter.value = 2;
return counter.value + logger.count;
}

/** Contains a store observed by the intervening call. */
public static int observedSnippet(Counter counter, Logger logger) {
counter.value = 1;
observingWork(counter, logger);
counter.value = 2;
return counter.value + logger.count;
}

/** Contains a store observable when the intervening call throws. */
public static int exceptionalSnippet(Counter counter, Logger logger) {
counter.value = 1;
potentiallyThrowingWork(logger);
counter.value = 2;
return counter.value + logger.count;
}

@Override
protected Suites createSuites(OptionValues options) {
Suites suites = super.createSuites(options).copy();
suites.getHighTier().prependPhase(new RefineInvokePhase());
return suites;
}

@Override
protected void checkMidTierGraph(StructuredGraph graph) {
super.checkMidTierGraph(graph);
int expectedWrites = graph.method().getName().equals(ELIMINATED_SNIPPET_NAME) ? 1 : 2;
Assert.assertEquals("Unexpected number of writes after dead-store elimination.", expectedWrites, graph.getNodes().filter(WriteNode.class).count());
}

/** Refines test invokes to model the relevant interprocedural summaries. */
private static final class RefineInvokePhase extends BasePhase<HighTierContext> {

@Override
public Optional<NotApplicable> notApplicableTo(GraphState graphState) {
return ALWAYS_APPLICABLE;
}

@Override
protected void run(StructuredGraph graph, HighTierContext context) {
for (InvokeNode invoke : graph.getNodes().filter(InvokeNode.class)) {
String targetName = invoke.getTargetMethod().getName();
switch (targetName) {
case UNRELATED_CALLEE_NAME -> {
invoke.setKilledLocationIdentity(MemoryKill.NO_LOCATION);
invoke.setLocationIdentity(MemoryKill.NO_LOCATION);
invoke.setGuaranteedNotToThrow();
invoke.setInlineControl(Invoke.InlineControl.Never);
}
case OBSERVING_CALLEE_NAME -> {
invoke.setKilledLocationIdentity(MemoryKill.NO_LOCATION);
invoke.setLocationIdentity(LocationIdentity.any());
invoke.setGuaranteedNotToThrow();
invoke.setInlineControl(Invoke.InlineControl.Never);
}
case THROWING_CALLEE_NAME -> {
invoke.setKilledLocationIdentity(MemoryKill.NO_LOCATION);
invoke.setLocationIdentity(MemoryKill.NO_LOCATION);
invoke.setInlineControl(Invoke.InlineControl.Never);
}
default -> {
}
}
}
}
}

/** Verifies elimination across a non-throwing call with disjoint effects. */
@Test
public void testDeadStoreAcrossInvoke() {
test(ELIMINATED_SNIPPET_NAME, new Counter(), new Logger());
}

/** Verifies that an intervening read keeps the first store observable. */
@Test
public void testStoreObservedByInvoke() {
test("observedSnippet", new Counter(), new Logger());
}

/** Verifies that a potentially throwing invoke keeps the first store observable. */
@Test
public void testStoreObservableOnException() {
test("exceptionalSnippet", new Counter(), new Logger());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2113,6 +2113,8 @@ private InvokeWithExceptionNode createInvokeWithException(LoopScope loopScope, I
try (InliningLog.UpdateScope updateScope = InliningLog.openUpdateScopeTrackingReplacement(graph.getInliningLog(), invokeNode)) {
assert updateScope != null;
InvokeWithExceptionNode replacement = graph.add(new InvokeWithExceptionNode(callTarget, null, invokeNode.bci(), invokeNode.stamp(NodeView.DEFAULT)));
replacement.setLocationIdentity(invokeNode.getLocationIdentity());
replacement.setLastLocationAccess(invokeNode.getLastLocationAccess());
replacement.setStateAfter(invokeNode.stateAfter());
replacement.setStateDuring(invokeNode.stateDuring());
replacement.setInlineControl(invokeNode.getInlineControl());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@

import jdk.graal.compiler.graph.Node;
import jdk.graal.compiler.nodes.graphbuilderconf.GraphBuilderContext;
import jdk.graal.compiler.nodes.memory.MemoryAccess;
import jdk.graal.compiler.nodes.memory.SingleMemoryKill;
import jdk.graal.compiler.nodes.spi.Lowerable;
import jdk.graal.compiler.nodes.type.StampTool;
import jdk.vm.ci.meta.ResolvedJavaMethod;
import jdk.vm.ci.meta.ResolvedJavaType;

public interface Invoke extends StateSplit, Lowerable, SingleMemoryKill, DeoptimizingNode.DeoptDuring, FixedNodeInterface, Invokable {
public interface Invoke extends StateSplit, Lowerable, SingleMemoryKill, MemoryAccess, DeoptimizingNode.DeoptDuring, FixedNodeInterface, Invokable {

String CYCLES_UNKNOWN_RATIONALE = "Cannot estimate the runtime cost of a call; it's a blackhole.";
String SIZE_UNKNOWN_RATIONALE = "Can only dynamically decide how much code is generated based on the type of a call (special, static, virtual, interface).";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
import jdk.graal.compiler.nodeinfo.NodeSize;
import jdk.graal.compiler.nodeinfo.Verbosity;
import jdk.graal.compiler.nodes.memory.AbstractMemoryCheckpoint;
import jdk.graal.compiler.nodes.memory.MemoryKill;
import jdk.graal.compiler.nodes.memory.SingleMemoryKill;
import jdk.graal.compiler.nodes.spi.LIRLowerable;
import jdk.graal.compiler.nodes.spi.NodeLIRBuilderTool;
Expand All @@ -74,32 +75,44 @@ public final class InvokeNode extends AbstractMemoryCheckpoint implements Invoke
@OptionalInput ValueNode classInit;
@Input(Extension) CallTargetNode callTarget;
@OptionalInput(State) FrameState stateDuring;
@OptionalInput(Memory) MemoryKill lastLocationAccess;
protected int bci;
protected boolean polymorphic;
protected InlineControl inlineControl;
protected final LocationIdentity identity;
/**
* The location killed by the invoke. Typically, it will be {@link LocationIdentity#any()}, but
* an interprocedural analysis can provide more precise location.
*/
protected LocationIdentity killedLocationIdentity;
/**
* The location accessed by the invoke. Typically, it will be {@link LocationIdentity#any()},
* but an interprocedural analysis can provide a more precise location.
*/
protected LocationIdentity locationIdentity;
private boolean isInOOMETry;
private boolean sideEffect;
private boolean guaranteedNotToThrow;

public InvokeNode(CallTargetNode callTarget, int bci) {
this(callTarget, bci, callTarget.returnStamp().getTrustedStamp());
}

public InvokeNode(CallTargetNode callTarget, int bci, LocationIdentity identity) {
this(callTarget, bci, callTarget.returnStamp().getTrustedStamp(), identity);
public InvokeNode(CallTargetNode callTarget, int bci, LocationIdentity killedLocationIdentity) {
this(callTarget, bci, callTarget.returnStamp().getTrustedStamp(), killedLocationIdentity);
}

public InvokeNode(CallTargetNode callTarget, int bci, Stamp stamp) {
this(callTarget, bci, stamp, LocationIdentity.any());
}

public InvokeNode(CallTargetNode callTarget, int bci, Stamp stamp, LocationIdentity identity) {
public InvokeNode(CallTargetNode callTarget, int bci, Stamp stamp, LocationIdentity killedLocationIdentity) {
super(TYPE, stamp);
this.callTarget = callTarget;
this.bci = bci;
this.polymorphic = false;
this.inlineControl = InlineControl.Normal;
this.identity = identity;
this.killedLocationIdentity = killedLocationIdentity;
this.locationIdentity = LocationIdentity.any();
this.sideEffect = super.hasSideEffect();
}

Expand Down Expand Up @@ -149,7 +162,34 @@ public Map<Object, Object> getDebugProperties(Map<Object, Object> map) {

@Override
public LocationIdentity getKilledLocationIdentity() {
return identity;
return killedLocationIdentity;
}

public void setKilledLocationIdentity(LocationIdentity identity) {
this.killedLocationIdentity = identity;
}

@Override
public LocationIdentity getLocationIdentity() {
return locationIdentity;
}

/**
* Sets the location accessed by this invoke.
*/
public void setLocationIdentity(LocationIdentity identity) {
this.locationIdentity = identity;
}

@Override
public MemoryKill getLastLocationAccess() {
return lastLocationAccess;
}

@Override
public void setLastLocationAccess(MemoryKill lla) {
updateUsagesInterface(lastLocationAccess, lla);
lastLocationAccess = lla;
}

@Override
Expand Down Expand Up @@ -271,6 +311,20 @@ public void setSideEffect(boolean sideEffect) {
this.sideEffect = sideEffect;
}

/**
* Records that an interprocedural analysis proved this invoke cannot throw an exception.
*/
public void setGuaranteedNotToThrow() {
guaranteedNotToThrow = true;
}

/**
* Returns whether an interprocedural analysis proved this invoke cannot throw an exception.
*/
public boolean isGuaranteedNotToThrow() {
return guaranteedNotToThrow;
}

@Override
public boolean hasSideEffect() {
return sideEffect;
Expand Down
Loading