@Retention(value=RUNTIME) @Target(value=METHOD) public @interface Specialization
Defines a method of a node subclass to represent one specialization of an operation. Multiple
specializations can be defined in a node representing an operation. A specialization defines
which kind of input is expected using the method signature and the annotation attributes. The
specialized semantics of the operation are defined using the body of the annotated Java method. A
specialization method must be declared in a class that is derived from Node
that
references a TypeSystem
. At least one specialization must be defined per operation. If no
specialization is valid for the given set of input values then an
UnsupportedSpecializationException
is thrown instead of invoking any specialization
method.
A specialization must have at least as many parameters as there are NodeChild
annotations
declared for the enclosing operation node. These parameters are declared in the same order as the
NodeChild
annotations (linear execution order). We call such parameters dynamic input
parameters. Every specialization that is declared within an operation must have an equal number
of dynamic input parameters.
The supported kind of input values for a specialization are declared using guards. A specialization may provide declarative specifications for four kinds of guards:
TypeSystem
. If the type of the parameter is Object
then no type guard is used for
the dynamic input parameter.true
. Expression guards are modeled using Java expressions that return a
boolean
value. If the guard expression returns false
, the
specialization is no longer applicable and the operation is re-specialized. Guard expressions are
declared using the Specialization.guards()
attribute.Specialization.rewriteOn()
attribute can be used to declare a list of such
exceptions. Guards of this kind are useful to avoid calculating a value twice when it is used in
the guard and its specialization.Assumption
remains true
. Assumptions can be assigned to specializations using the
Specialization.assumptions()
attribute.
The enclosing Node
of a specialization method must have at least one public
and non-final
execute method. An execute method is a method that starts with
'execute'. If all execute methods declare the first parameter type as Frame
,
VirtualFrame
or MaterializedFrame
then the same frame type can be used as
optional first parameter of the specialization. This parameter does not count to the number of
dynamic parameters.
A specialization method may declare multiple parameters annotated with Cached
. Cached
parameters are initialized and stored once per specialization instantiation. For consistency
between specialization declarations cached parameters must be declared last in a specialization
method.
If the operation is re-specialized or if it is executed for the first time then all declared specializations of the operation are tried in declaration order until the guards of the first specialization accepts the current input values. The new specialization is then added to the chain of current specialization instances which might consist of one (monomorph) or multiple instances (polymorph). If an assumption of an instantiated specialization is violated then re-specialization is triggered again.
With guards in combination with cached parameters it is possible that multiple instances of the
same specialization are created. The Specialization.limit()
attribute can be used to limit the number
of instantiations per specialization.
NodeChild
,
Fallback
,
Cached
,
TypeSystem
,
TypeSystemReference
,
UnsupportedSpecializationException
Modifier and Type | Optional Element and Description |
---|---|
String[] |
assumptions
Declares assumption guards that optimistically assume that the state of an
Assumption
remains valid. |
String[] |
guards
Declares
boolean expressions that define whether or not input values are
applicable to this specialization instance. |
String |
insertBefore
References a specialization of a super class by its method name where this specialization is
inserted before.
|
String |
limit
Declares the expression that limits the number of specialization instantiations.
|
String[] |
replaces
Declares other specializations of the same operation to be replaced by this specialization.
|
Class<? extends Throwable>[] |
rewriteOn
Declares an event guards that trigger re-specialization in case an exception is thrown in the
specialization body.
|
int |
unroll
Instructs the specialization to unroll a specialization with multiple instances.
|
public abstract String insertBefore
public abstract Class<? extends Throwable>[] rewriteOn
Declares an event guards that trigger re-specialization in case an exception is thrown in the specialization body. This attribute can be used to declare a list of such exceptions. Guards of this kind are useful to avoid calculating a value twice when it is used in the guard and its specialization.
If an event guard exception is triggered then all instantiations of this specialization are
removed. If one of theses exceptions is thrown once then no further instantiations of this
specialization are going to be created for this node.
In case of explicitly declared UnexpectedResultException
s, the result from the
exception will be used. For all other exception types, the next available specialization will
be executed, so that the original specialization must ensure that no non-repeatable
side-effect is caused until the rewrite is triggered.
@Specialization(rewriteOn = ArithmeticException.class) int doAddNoOverflow(int a, int b) { return Math.addExact(a, b); } @Specialization long doAddWithOverflow(int a, int b) { return a + b; } ... Example executions: execute(Integer.MAX_VALUE - 1, 1) => doAddNoOverflow(Integer.MAX_VALUE - 1, 1) execute(Integer.MAX_VALUE, 1) => doAddNoOverflow(Integer.MAX_VALUE, 1) => throws ArithmeticException => doAddWithOverflow(Integer.MAX_VALUE, 1) execute(Integer.MAX_VALUE - 1, 1) => doAddWithOverflow(Integer.MAX_VALUE - 1, 1)
Math.addExact(int, int)
public abstract String[] replaces
Declares other specializations of the same operation to be replaced by this specialization. Other specializations are referenced using their unique method name. If this specialization is instantiated then all replaced specialization instances are removed and never instantiated again for this node instance. Therefore this specialization should handle strictly more inputs than which were handled by the replaced specialization, otherwise the removal of the replaced specialization will lead to unspecialized types of input values. The replaces declaration is transitive for multiple involved specializations.
Example usage:@Specialization(guards = "b == 2") void doDivPowerTwo(int a, int b) { return a >> 1; } @Specialization(replaces ="doDivPowerTwo", guards = "b > 0") void doDivPositive(int a, int b) { return a / b; } ... Example executions with replaces="doDivPowerTwo": execute(4, 2) => doDivPowerTwo(4, 2) execute(9, 3) => doDivPositive(9, 3) // doDivPowerTwo instances get removed execute(4, 2) => doDivPositive(4, 2) Same executions without replaces="doDivPowerTwo" execute(4, 2) => doDivPowerTwo(4, 2) execute(9, 3) => doDivPositive(9, 3) execute(4, 2) => doDivPowerTwo(4, 2)
Specialization.guards()
public abstract String[] guards
Declares boolean
expressions that define whether or not input values are
applicable to this specialization instance. Guard expressions must always return the same
result for each combination of the enclosing node instance and the bound input values.
If a guard expression does not bind any dynamic input parameters then the DSL, by default,
assumes that the result will not change for this node after specialization instantiation. In
other words the DSL assumes idempotence for this guard on the fast-path, by default. The
Idempotent
and NonIdempotent
annotations may be used to configure this
explicitly. The DSL will also emit warnings in case the use of such annotations is
recommended. If assertions are enabled (-ea), then the DSL will assert that the idempotence
property does hold at runtime.
Guard expressions are defined using a subset of Java. This subset includes field/parameter
accesses, function calls, type exact infix comparisons (==, !=, <, <=, >, >=), logical
negation (!), logical disjunction (||), null, true, false, and integer literals. The return
type of guard expressions must be boolean
. Bound elements without receivers are
resolved using the following order:
NodeField
for the enclosing node.ImportStatic
.Example usage:
static boolean acceptOperand(int operand) { assert operand <= 42; return (operand & 1) == 1; } @Specialization(guards = {"operand <= 42", "acceptOperand(operand)"}) void doSpecialization(int operand) {...}
Cached
,
ImportStatic
public abstract String[] assumptions
Declares assumption guards that optimistically assume that the state of an Assumption
remains valid. Assumption expressions are cached once per specialization instantiation. If
one of the returned assumptions gets invalidated then the specialization instance is removed.
If the assumption expression returns an array of assumptions then all assumptions of the
array are checked. This is limited to one-dimensional arrays.
Assumption expressions are defined using a subset of Java. This subset includes
field/parameter accesses, function calls, type exact infix comparisons (==, !=, <, <=, >,
>=), logical negation (!), logical disjunction (||), null, true, false, and integer literals.
The return type of the expression must be Assumption
or an array of
Assumption
instances. Assumption expressions are not allowed to bind to dynamic
parameter values of the specialization. Bound elements without receivers are resolved using
the following order:
NodeField
for the enclosing node.ImportStatic
.Example usage:
abstract static class DynamicObject() { abstract Shape getShape(); ... } abstract static class Shape() { abstract Assumption getUnmodifiedAssuption(); ... } @Specialization(guards = "operand.getShape() == cachedShape", assumptions = "cachedShape.getUnmodifiedAssumption()") void doAssumeUnmodifiedShape(DynamicObject operand, @Cached("operand.getShape()") Shape cachedShape) {...}
Cached
,
ImportStatic
public abstract String limit
Declares the expression that limits the number of specialization instantiations. The default
limit for specialization instantiations is defined as "3"
. If the limit is
exceeded no more instantiations of the enclosing specialization method are created. Please
note that the existing specialization instantiations are not removed from the
specialization chain. You can use Specialization.replaces()
to remove unnecessary specializations
instances.
The limit expression is defined using a subset of Java. This subset includes field/parameter
accesses, function calls, type exact infix comparisons (==, !=, <, <=, >, >=), logical
negation (!), logical disjunction (||), null, true, false, and integer literals. The return
type of the limit expression must be int
. Limit expressions are not allowed to
bind to dynamic parameter values of the specialization. Bound elements without receivers are
resolved using the following order:
NodeField
for the enclosing node.ImportStatic
.Example usage:
static int getCacheLimit() { return Integer.parseInt(System.getProperty("language.cacheLimit")); } @Specialization(guards = "operand == cachedOperand", limit = "getCacheLimit()") void doCached(Object operand, @Cached("operand") Object cachedOperand) {...}
Specialization.guards()
,
Specialization.replaces()
,
Cached
,
ImportStatic
public abstract int unroll
A common use-case for this feature is to unroll the first instance of an inline cache. It is often the case that specializations with multiple instances are instantiated only once. By unrolling the first instance we can optimize for this common situation which may lead to footprint and interpreter performance improvements.
This feature is prone to cause inefficiencies if used too aggressively. Extra care should be taken, e.g. the generated code should be inspected and profiled to verify that the new code is better than the previous version.
Consider the following example:
class MyNode extends Node { static int limit = 2; abstract int execute(int value); @Specialization(guards = "value == cachedValue", limit = "limit", unroll = 1) int doDefault(int value, @Cached("value") int cachedValue) { return value; } }In this example we unroll the first instance of an inline cache on
int
values.
This is equivalent to manually specifying the following specializations:
class MyUnrollNode extends Node { static int limit = 2; abstract int execute(int value); @Specialization(guards = "value == cachedValue", limit = "1") int doUnrolled0(int value, @Cached("value") int cachedValue) { return value; } @Specialization(guards = "value == cachedValue", limit = "limit - 1") int doDefault(int value, @Cached("value") int cachedValue) { return value; } }