- GraalVM for JDK 24 (Latest)
- GraalVM for JDK 25 (Early Access)
- GraalVM for JDK 21
- GraalVM for JDK 17
- Archives
- Dev Build
- Truffle Language Implementation Framework
- Truffle Branches Instrumentation
- Dynamic Object Model
- Static Object Model
- Host Optimization for Interpreter Code
- Truffle Approach to Function Inlining
- Profiling Truffle Interpreters
- Truffle Interop 2.0
- Language Implementations
- Implementing a New Language with Truffle
- Truffle Languages and Instruments Migration to Java Modules
- Truffle Native Function Interface
- Optimizing Truffle Interpreters
- Options
- On-Stack Replacement
- Truffle Strings Guide
- Specialization Histogram
- Testing DSL Specializations
- Polyglot API Based TCK
- Truffle Approach to the Compilation Queue
- Truffle Library Guide
- Truffle AOT Overview
- Truffle AOT Compilation
- Auxiliary Engine Caching
- Truffle Language Safepoint Tutorial
- Monomorphization
- Splitting Algorithm
- Monomorphization Use Cases
- Reporting Polymorphic Specializations to Runtime
This documentation is for the unreleased GraalVM version.Download Early Access Builds from GitHub.
Deoptimization Cycle Patterns
As of GraalVM for JDK 25, the Truffle framework includes an automatic deoptimization cycle detection feature, which is a powerful tool for identifying existing deoptimization cycles. The goal of this document is to help prevent deoptimization cycles by describing common patterns that can lead to such cycles, which should be avoided.
Always Deoptimizing Node
One of the simplest patterns that causes a deoptimization cycle is as follows:
class AlwaysDeoptNode extends RootNode {
@Override
public Object execute(VirtualFrame frame) {
CompilerDirectives.transferToInterpreterAndInvalidate();
return null;
}
}
When the above code is compiled, it deoptimizes and invalidates on the first execution. The same occurs with each subsequent compilation and execution. This is an extreme example, and it is hard to imagine how the code could be useful for anything other than testing purposes, but it is a clear cause of unbounded repeated deoptimization—that is, a deoptimization cycle.
Invalid Argument Profiling Node
The following is an example of non-stabilizing argument profiling:
class InvalidArgumentProfilingNode extends RootNode {
@CompilerDirectives.CompilationFinal int cachedValue;
@Override
public Object execute(VirtualFrame frame) {
int arg = (int) frame.getArguments()[0];
if (this.cachedValue != arg) {
// Bug: repeated non-stabilizing deoptimization
CompilerDirectives.transferToInterpreterAndInvalidate();
this.cachedValue = arg;
}
return this.cachedValue;
}
}
The number of deoptimizations and invalidations in the above code is unbounded because the profiled argument can keep changing with no limit on the number of these changes. Consequently, there is no bound on the number of deoptimizations.
A possible solution to this problem would be to use IntValueProfile
, which caches the value only once and switches to a generic state when the value changes.
Stabilize Late Node
The following code does not cause a true deoptimization cycle. It eventually stabilizes, but stabilization occurs so late that the deoptimization cycle detection tool might still report a cycle:
class StabilizeLateNode extends RootNode {
static final int GENERIC = Integer.MIN_VALUE;
@CompilationFinal int cachedValue;
int seenCount = 0;
@Override
public Object execute(VirtualFrame frame) {
int arg = (int) frame.getArguments()[0];
// we assume Integer.MIN_VALUE is never used in value
assert arg != Integer.MIN_VALUE;
int result;
if (cachedValue == GENERIC) {
result = arg;
} else {
if (cachedValue != arg) {
CompilerDirectives.transferToInterpreterAndInvalidate();
if (seenCount < 20) {
this.cachedValue = arg;
} else {
cachedValue = GENERIC;
}
seenCount++;
result = arg;
} else {
result = cachedValue;
}
}
return result;
}
}
After 20 consecutive executions where the argument arg
is different from cachedValue
, the variable seenCount
reaches 20. At this point, deoptimizations stop.
Until then, the code may be compiled and deoptimized once for each seenCount
value below 20—up to twenty times.
Since deoptimization cycle detection is enabled by default after 15 compilations, it could be triggered by this late-stabilizing code.
The solution to this problem is to ensure that the optimizations stabilize more quickly.
Compilation Final Field of a Non-Constant Object
Truffle compilation final fields are a powerful optimization tool, often used to control speculations. For example, they are used in conditions checking whether a certain specialized piece of code is valid for the current input. When it is not, the compiled code is deoptimized and invalidated. This allows the code to re-specialize and recompile with the updated assumption. However, this usage must follow one key rule:
- The number of possible deoptimizations (and invalidations) caused by the condition involving a compilation final field should be small. As seen in the previous examples, failing to follow this rule can lead to issues.
Extra caution must be taken when the object containing the compilation field is not a partial evaluation (PE) constant because, in that case, the field is not compilation-final, and reading it translates to a normal field read. This makes it more difficult to reason about the number of possible deoptimizations caused by a condition involving the field.
The following examples demonstrate what can happen if this caution is not exercised.
Example 1: Non-Constant Language Context
The following code shows repeated deoptimization caused by the initialization of each new language context:
class LanguageContext {
private final Env env;
@CompilationFinal boolean initialized;
LanguageContext(Env env) {
this.env = env;
}
void ensureInitialized() {
if (!initialized) {
CompilerDirectives.transferToInterpreterAndInvalidate();
initialize();
}
}
private void initialize() {
// perform initialization
initialized = true;
}
static final ContextReference<LanguageContext> REF = ContextReference.create(MyLanguage.class);
}
class LanguageContextDrivenInitialization extends RootNode {
@Override
public Object execute(VirtualFrame frame) {
LanguageContext languageContext = LanguageContext.REF.get(this);
languageContext.ensureInitialized();
// ...
return null;
}
}
If TruffleLanguage.Registration.contextPolicy
of MyLanguage
is SHARED
, a new language context is created for each polyglot context used to run the language code.
Therefore, languageContext
is not a PE constant and so the number of deoptimizations (and invalidations) driven by the initialized
boolean is unbounded.
A possible solution for this problem would be to transfer to the interpreter only when languageContext
is a PE constant, and to annotate the initialize method with TruffleBoundary
:
class LanguageContext {
private final Env env;
@CompilationFinal boolean initialized;
LanguageContext(Env env) {
this.env = env;
}
void ensureInitialized() {
if (!initialized) {
if (CompilerDirectives.isPartialEvaluationConstant(this)) {
CompilerDirectives.transferToInterpreterAndInvalidate();
}
initialize();
}
}
@TruffleBoundary
void initialize() {
// perform initialization
initialized = true;
}
static final ContextReference<LanguageContext> REF = ContextReference.create(MyLanguage.class);
}
class LanguageContextDrivenInitialization extends RootNode {
@Override
public Object execute(VirtualFrame frame) {
LanguageContext languageContext = LanguageContext.REF.get(this);
languageContext.ensureInitialized();
// ...
return null;
}
}
Annotating the method with TruffleBoundary
has the downside of being slow when languageContext
is not a PE constant, as it would always produce a runtime call after compilation.
Another solution could be to perform the initialization eagerly, which would avoid the problem entirely.
Example 2: Dispatch Node
The following code shows repeated deoptimization caused by the initialization of call targets in a dispatch node:
class MyFunction {
private final RootNode root;
}
class DispatchNode extends RootNode {
@Child IndirectCallNode callNode = Truffle.getRuntime().createIndirectCallNode();
@Override
public Object execute(VirtualFrame frame) {
MyFunction function = (MyFunction) frame.getArguments()[0];
CallTarget target = function.root.getCallTarget();
return callNode.call(target);
}
}
// Simplified version of the RootNode class from the Truffle framework. It is not complete, only relevant parts are listed.
class RootNode {
@CompilationFinal private volatile RootCallTarget callTarget;
public final RootCallTarget getCallTarget() {
if (callTarget == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
initializeTarget();
}
return callTarget;
}
}
The dispatch node has a single call site, and each uninitialized call target must be initialized before the call.
Initialization is performed in the getCallTarget
method if the compilation final field callTarget
on the callee’s RootNode
object is null
.
The callee’s root node object is not a PE constant.
In fact, the number of root nodes the compiled code can see is theoretically unbounded.
Initializing a call target causes a deoptimization, so the entire pattern leads to a deoptimization cycle.
A possible solution to this problem would be to initialize the call target at parse time or when the function is looked up for the first time.
MyFunction
objects would then store the call targets directly:
class MyFunction {
private final CallTarget target;
}
class DispatchNode extends RootNode {
@Child IndirectCallNode callNode = IndirectCallNode.create();
@Override
public Object execute(VirtualFrame frame) {
MyFunction funcion = (MyFunction) frame.getArguments()[0];
CallTarget target = function.target;
return callNode.call(target);
}
}
Skipped Exceptions
The following code causes a deoptimization by throwing a skipped exception:
class SkippedExceptionNode extends RootNode {
@Override
public Object execute(VirtualFrame frame) {
try {
throw new IndexOutOfBoundsException();
} catch (RuntimeException e) {
//
}
return null;
}
}
Skipped exceptions are exceptions that always cause a deoptimization. The complete list is as follows:
UnexpectedResultException
SlowPathException
ScopedMemoryAccess$ScopedAccessError
ArithmeticException
IllegalArgumentException
IllegalStateException
VirtualMachineError
IndexOutOfBoundsException
ClassCastException
BufferUnderflowException
BufferOverflowException
ReadOnlyBufferException
AssertionError
As you can see from the example, catching the exception does not help. Whenever a skipped exception is thrown in Truffle-compiled code, the code is deoptimized and invalidated, and repeated execution of such code leads to a deoptimization cycle.
The solution to this problem is to ensure that skipped exceptions are never thrown from compiled code. You can achieve this by either:
- Throwing the exception behind
TruffleBoundary
, or - Using explicit guards to guarantee that the exception cannot be thrown in compiled code.