Skip to content

Conversation

@blaumeise20
Copy link
Contributor

This PR adds a method CompilerDirectives.mergeExplodeKey which can be used to explicitly mark variables to be used as the merge key in MERGE_EXPLODE loop explosion.

Currently, the entire frame state is used for merging. Taking Espresso as an example, if you would jump to the same bci but assign a different value to another variable, it will result in two separate code paths. This can lead to unexpected graph size explosion and bugs. With the method introduced in this PR, you can mark a single variable to be used as the key. Truffle will merge on this key only, but verify that other variables stay the same. If merging should occur but there is a mismatch, the compilation will bail out.

Future steps include allowing multiple variables to be marked simultaneously (which has to be extended into irreducible loop handling), and creating an option for explicit PHI nodes, so local variables can be used as counters without the need for objects.

@oracle-contributor-agreement oracle-contributor-agreement bot added the OCA Verified All contributors have signed the Oracle Contributor Agreement. label Nov 18, 2025
@blaumeise20
Copy link
Contributor Author

@gilles-duboscq

Copy link
Member

@gilles-duboscq gilles-duboscq left a comment

Choose a reason for hiding this comment

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

Please also check the 2 failed gates

* @param i the variable that should be used as the merge key.
* @return the unchanged value
*/
@SuppressWarnings("unused")
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need this?

* the value must be a constant integer.
*
* @param i the variable that should be used as the merge key.
* @return the unchanged value
Copy link
Member

Choose a reason for hiding this comment

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

Please add @since 25.1

@Override
public boolean apply(GraphBuilderContext b, ResolvedJavaMethod targetMethod, Receiver receiver, ValueNode value) {
if (canDelayIntrinsification) {
return false;
Copy link
Member

Choose a reason for hiding this comment

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

I think this deserves a comment explaining why we do this.

import jdk.graal.compiler.nodes.spi.CanonicalizerTool;

@NodeInfo(nameTemplate = "LoopExplosionKey", cycles = CYCLES_0, size = SIZE_0)
public final class LoopExplosionKeyNode extends FloatingNode implements Canonicalizable {
Copy link
Member

Choose a reason for hiding this comment

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

What happens if the directive is used in a non-merge-explode method and this node remains in the graph?
I think this should either be a very explicit compilation failure or performance warning. If it's only a warning then we need to remove such nodes.

@chumer what do you think? Should using CompilationDirective.mergeExplodeKey outside of the context of a @ExplodeLoop(kind = LoopExplosionKind.MERGE_EXPLODE) method be a compilation error or a performance warning?

}
}

graph.maybeMarkUnsafeAccess(encodedGraph);
Copy link
Member

Choose a reason for hiding this comment

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

Why did you remove this?

LoopExplosionState queryState = new LoopExplosionState(frameState, null);
LoopExplosionState existingState = loopScope.iterationStates.get(queryState);
if (loopScope.trigger == LoopScopeTrigger.START) {
List<Integer> mergeKey = null;
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
List<Integer> mergeKey = null;
List<Integer> mergeKeys = null;

for (int i : loopScope.loopExplosionMergeKeySlots) {
ValueNode node = frameState.values().get(i);
if (!node.isConstant() || node.asJavaConstant().getJavaKind() != JavaKind.Int) {
throw new PermanentBailoutException("Graal implementation restriction: Method with %s loop explosion must have a loop variable of type int. %s",
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
throw new PermanentBailoutException("Graal implementation restriction: Method with %s loop explosion must have a loop variable of type int. %s",
throw new PermanentBailoutException("Graal implementation restriction: merge keys must partial evaluate to int constants at the loop header. %s",

values = new ArrayList<>(loopScope.loopExplosionMergeKeySlots.size());
for (int i : loopScope.loopExplosionMergeKeySlots) {
ValueNode node = frameState.values().get(i);
if (!node.isConstant() || node.asJavaConstant().getJavaKind() != JavaKind.Int) {
Copy link
Member

Choose a reason for hiding this comment

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

Should we accept all integer types (i.e., also accept longs)?
What do you think @chumer?

// merge has a frame state
loop.exits.removeIf(x -> x.merge() == merge);
loop.exits.add(end);
continue;
Copy link
Member

Choose a reason for hiding this comment

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

this continue doesn't look necessary

livenessAnalysis.onStart(frame, skipLivenessActions);
}

curBCI = CompilerDirectives.mergeExplodeKey(curBCI);
Copy link
Member

Choose a reason for hiding this comment

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

This could be moved to a separate commit

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

Labels

OCA Verified All contributors have signed the Oracle Contributor Agreement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants