Truffle DSLノード・オブジェクトのインライン化
23.0では、@GenerateInline
という新しい注釈が導入されました。この注釈は、Truffle DSL注釈プロセッサに、ノードのインライン化可能なバージョンを生成するように指示します。これは、キャッシュされたノード・バージョンまたはキャッシュされていないノード・バージョンを生成する@GenerateCached
および@GenerateUncached
と同様に機能します。デフォルトでは、DSLはノードのインライン化されたバージョンを生成しません。ノードのインライン化は、ノードのメモリー・フットプリントを削減する簡単な方法を提供しますが、多くの場合、インタプリタの実行速度も向上させます。
基本的な使用方法
2つの値の絶対値の合計を計算する特殊化を持つノードがあるとします。わかりやすくするために、この例ではlong
型の特殊化のみを見ていきます。
この例の実行可能だが少し高度なバージョンは、Truffleユニット・テストにあります。
- NodeInliningExample1_1.javaは、インライン化を行わない例を示しています。
- NodeInliningExample1_2.javaは、部分インライン化を行わない例を示しています。
- NodeInliningExample1_3.javaは、完全インライン化を行う例を示しています。
特殊化する2つの通常のノードを指定する次の例を考えてみます。1つのノードは2つの値の合計を計算し、もう1つのノードは数値の絶対数を計算します。AbsNode
はAddAbsNode
で再利用され、実装を共有します。
public abstract class AddAbsNode extends Node {
abstract long execute(Object left, Object right);
@Specialization
long add(long left, long right,
@Cached AbsNode leftAbs,
@Cached AbsNode rightAbs) {
return leftAbs.execute(left) + rightAbs.execute(right);
}
// ...
}
public abstract class AbsNode extends Node {
abstract long execute(long value);
@Specialization(guards = "v >= 0")
long doInt(long v) {
return v;
}
@Specialization(guards = "v < 0")
long doLong(long v) {
return -v;
}
}
1回の実行後のAbsNode
およびAddAbsNode
の圧縮メモリー・フットプリントは、次のように計算されます:
AbsNodeGen = object header
+ Node field for Node.parent
+ int field for state
AddAbsNodeGen = object header
+ Node field for Node.parent
+ int field for state
+ Node field for @Cached AbsNode leftAbs
+ Node field for @Cached AbsNode rightAbs
Footprint = headerCount * 12 + pointerCount * 4 + primitiveByteSize
Footprint = 3 * 12 + 5 * 4 + 12 = 68 bytes
したがって、68
バイトを使用して、ノードによる単一の操作を表します。
23.0では、Truffle DSL注釈プロセッサによって、AbsNode
クラスに対して次の警告が生成されます:
This node is a candidate for node object inlining. The memory footprint is estimated to be reduced from 20 to 1 byte(s). Add @GenerateInline(true) to enable object inlining for this node or @GenerateInline(false) to disable this warning. Also, consider disabling cached node generation with @GenerateCached(false) if all usages will be inlined. This warning may be suppressed using @SuppressWarnings("truffle-inlining").
この警告の推奨事項に従って、@GenerateInline
注釈を追加して、例を次のように変更します:
@GenerateInline
public abstract class AbsNode extends Node {
abstract long execute(long value);
@Specialization(guards = "v >= 0")
long doInt(long v) {
return v;
}
@Specialization(guards = "v < 0")
long doLong(long v) {
return -v;
}
}
これで、DSLによってAbsNode
のコンパイル・エラーが報告されます:
Error generating code for @GenerateInline: Found non-final execute method without a node parameter execute(long). Inlinable nodes
must use the Node type as the first parameter after the optional frame for all non-final execute methods. A valid signature for an
inlinable node is execute([VirtualFrame frame, ] Node node, ...).
インライン化可能なノードの場合、最初のパラメータとしてノード・パラメータを実行メソッドに渡す必要があります。これは、インライン化されたノードがシングルトンになり、独自の状態ではなくなりますが、実行メソッドにパラメータとして渡されるため、必要です。
ここでも、エラーに従い、例を次のように変更します:
@GenerateInline
public abstract class AbsNode extends Node {
abstract long execute(Node node, long value);
@Specialization(guards = "v >= 0")
static long doInt(long v) {
return v;
}
@Specialization(guards = "v < 0")
static long doLong(long v) {
return -v;
}
}
ノード・パラメータは特殊化メソッドではオプションですが、通常は、推移的にインライン化されたノードを使用する場合に必要です。
次に、AddAbsNode
を変更して、this
をノード・パラメータとして新しい実行シグネチャに渡す必要もあります:
public abstract static class AddAbsNode extends Node {
abstract long execute(long left, long right);
@Specialization
long add(long left, long right,
@Cached AbsNode leftAbs,
@Cached AbsNode rightAbs) {
return leftAbs.execute(this, left) + rightAbs.execute(this, right);
}
// ...
}
DSLでは、@Cached AbsNode
パラメータごとに警告が生成されるようになりました:
The cached type 'AbsNode' supports object-inlining. The footprint is estimated to be reduced from 36 to 1 byte(s). Set @Cached(..., inline=true|false) to determine whether object-inlining should be performed. Alternatively, @GenerateCached(alwaysInlineCached=true) can be used to enable inlining for an entire class or in combination with the inherit option for a hierarchy of node classes. This warning may be suppressed using @SuppressWarnings("truffle-inlining").
このメッセージの推奨事項に従い、オブジェクトのインライン化を有効にします:
public abstract static class AddAbsNode extends Node {
abstract long execute(long left, long right);
@Specialization
long add(long left, long right,
@Cached(inline = true) AbsNode leftAbs,
@Cached(inline = true) AbsNode rightAbs) {
return leftAbs.execute(this, left) + rightAbs.execute(this, right);
}
// ...
}
これで、AbsNode
をAddAbsNode
にオブジェクト・インライン化できました。新しいメモリー・フットプリントは次のように計算されます:
AddAbsNodeGen = object header
+ Node field for Node.parent
+ int field for state
Footprint = headerCount * 12 + pointerCount * 4 + primitiveByteSize
Footprint = 1 * 12 + 1 * 4 + 4 = 20 bytes
フットプリントは、AddAbsNodeGen
のインスタンスごとに68
バイトから20
バイトのみに減少しました。
しかし、引き続き進めていきます。キャッシュされたすべてのノードはインライン化されるため、AddAbsNode
を用途に応じてインライン化することもできます。DSLは、このようなケースを検出してAddAbsNode
の警告を出力することで、再度役立ちます:
This node is a candidate for node object inlining. The memory footprint is estimated to be reduced from 20 to 1 byte(s). Add @GenerateInline(true) to enable object inlining for this node or @GenerateInline(false) to disable this warning. Also consider disabling cached node generation with @GenerateCached(false) if all usages will be inlined. This warning may be suppressed using @SuppressWarnings("truffle-inlining").
ここでも、ガイドに従い、@GenerateInline
注釈をAddAbsNode
に追加します。前述の場合と同様に、Node
パラメータも実行メソッドに追加します:
@GenerateInline
public abstract static class AddAbsNode extends Node {
abstract long execute(Node node, long left, long right);
@Specialization
static long add(Node node, long left, long right,
@Cached AbsNode leftAbs,
@Cached AbsNode rightAbs) {
return leftAbs.execute(node, left) + rightAbs.execute(node, right);
}
// ...
}
また、特殊化メソッドでNode
パラメータを使用し、子ノードに渡す必要もあります。この場合も、this
を誤って渡さないように、すべての特殊化をstatic
にしてください。さらに、DSLではinline=true
属性についてエラーが発生していましたが、親ノードが@GenerateInline
注釈を使用するため、常に暗黙的に示されるようになりました。
新しいインライン化可能なAddAbsNode
ノードのオーバーヘッドを測定するために、AddAbsNode
操作を使用して4つの数値を追加するAdd4AbsNode
という新しい操作を宣言します:
@GenerateCached(alwaysInlineCached = true)
public abstract static class Add4AbsNode extends Node {
abstract long execute(long v0, long v1, long v2, long v3);
@Specialization
long doInt(long v0, long v1, long v2, long v3,
@Cached AddAbsNode add0,
@Cached AddAbsNode add1,
@Cached AddAbsNode add2) {
long v;
v = add0.execute(this, v0, v1);
v = add1.execute(this, v, v2);
v = add2.execute(this, v, v3);
return v;
}
}
今回は、@Cached(inline=true)
を指定するかわりに、@GenerateCached(alwaysInlineCached = true)
を使用して可能なかぎりインライン化を自動的に有効化します。ユースケースによっては、キャッシュされたノードごとに個々のインライン化コマンドを繰り返すと、読みやすさが損なわれる可能性があります。
オーバーヘッドの計算は、より複雑になっています。各ノードがアクティブな特殊化を追跡するために必要な状態ビット数を理解する必要があります。通常、その計算は実装に固有であり、変更される可能性があります。ただし、経験則として、DSLでは宣言された特殊化ごとに1ビットが必要です。暗黙的なキャスト、置換ルール、@Fallback
および複数のインスタンスを使用する特殊化によって、必要な状態ビットの数がさらに増える可能性があります。
この例では、各AddAbsNode
に5ビットが必要です。AbsNode
の使用ごとに2ビット、AddAbsNode
の特殊化に1ビットです。Add4AbsNode
は、AddAbsNode
の3つのインスタンスを使用し、1つの特殊化があるため、合計で3 * 5 + 1
状態ビットが必要です。ビット数が32を下回るため、生成されたコードには単一のint
フィールドが必要であると想定できます。したがって、実行されたAdd4AbsNode
のメモリー・フットプリントは次のように計算されます:
Footprint = 1 * 12 + 1 * 4 + 4 = 20 bytes
ご覧のとおり、これは単一のAddAbsNode
のメモリー・フットプリントと同じです。同じ式を使用して、オブジェクトのインライン化を行わずにAdd4AbsNode
のメモリー・フットプリントを計算する場合
Footprint = 1 * 12 + 4 * 4 + 4 + 3 * 68 = 236 bytes
オーバーヘッドを236
バイトから20
バイトに削減しました。
メモリー・フットプリントの利点に加えて、インタプリタのみの実行が高速になる可能性があります。これは、ノード・フィールドの読取りを節約し、メモリー消費量が少なくなりCPUキャッシュ局所性が向上するためです。部分評価を使用してコンパイルすると、キャッシュされたバージョンとキャッシュされていないバージョンの両方が同じように実行されることが予想されます。
最後に次を行います。AddAbsNode
およびAbsNode
はキャッシュされたバージョンで使用されなくなったため、@GenerateCached(false)
を使用してキャッシュされた生成をオフにすると、Javaコード・フットプリントを節約できます。これを行うと、インライン化されたバージョンのみが使用可能な場合、ノードは自動的にインライン化されるため、@GenerateCached
注釈のalwaysInlineCached
プロパティを省略できます。
最後の例を次に示します:
@GenerateInline
@GenerateCached(false)
public abstract static class AbsNode extends Node {
abstract long execute(Node node, long value);
@Specialization(guards = "v >= 0")
static long doInt(long v) {
return v;
}
@Specialization(guards = "v < 0")
static long doLong(long v) {
return -v;
}
}
@GenerateInline
@GenerateCached(false)
public abstract static class AddAbsNode extends Node {
abstract long execute(Node node, long left, long right);
@Specialization
static long add(Node node, long left, long right,
@Cached AbsNode leftAbs,
@Cached AbsNode rightAbs) {
return leftAbs.execute(node, left) + rightAbs.execute(node, right);
}
// ...
}
@GenerateCached(alwaysInlineCached = true)
@GenerateInline(false)
public abstract static class Add4AbsNode extends Node {
abstract long execute(long v0, long v1, long v2, long v3);
@Specialization
long doInt(long v0, long v1, long v2, long v3,
@Cached AddAbsNode add0,
@Cached AddAbsNode add1,
@Cached AddAbsNode add2) {
long v;
v = add0.execute(this, v0, v1);
v = add1.execute(this, v, v2);
v = add2.execute(this, v, v3);
return v;
}
}
なお、DSLでは、Add4AbsNode
が@GenerateInline
を使用できることが、次の警告の発行によって再度通知されました:
This node is a candidate for node object inlining. The memory footprint is estimated to be reduced from 20 to 2 byte(s). Add @GenerateInline(true) to enable object inlining for this node or @GenerateInline(false) to disable this warning. Also consider disabling cached node generation with @GenerateCached(false) if all usages will be inlined. This warning may be suppressed using @SuppressWarnings("truffle-inlining").
今回は、@GenerateInline(false)
を明示的に指定することで警告を抑制しました。
インライン・キャッシュの高度な使用方法
次の例では、特殊化のアンロールおよび新しいインライン化可能なキャッシュ・クラスが、複数のインスタンスを持つ特殊化を使用して、ノードのメモリー・フットプリントを削減する際にどのように役立つかを説明します。
例:
- NodeInliningExample2_1.javaは、インライン化を行わない例を示しています。
- NodeInliningExample2_2.javaは、部分インライン化を行わない例を示しています。
- NodeInliningExample2_3.javaは、完全インライン化を行う例を示しています。
ノードの適切な受渡し
インライン化されたノードを使用するには、それぞれのインライン化されたノードのメソッドを実行するために、正しいノードにアクセスして渡す必要があります。メソッドを実行する際に誤ったノードを渡してしまうのは、よくある間違いです。通常、このような誤りは実行時にエラーで失敗しますが、DSLはコンパイル時に状況に応じて警告とエラーも出力します。
インライン化されたノード
インライン化されたノード自体を使用するインライン化されたノードの場合、Node
動的パラメータにlongを渡すだけで十分です。例えば、前の項ではAddAbsNode
を同様のパターンで使用しました:
@GenerateInline
@GenerateCached(false)
public abstract static class AddAbsNode extends Node {
abstract long execute(Node node, long left, long right);
@Specialization
static long add(Node node, long left, long right,
@Cached AbsNode leftAbs,
@Cached AbsNode rightAbs) {
return leftAbs.execute(node, left) + rightAbs.execute(node, right);
}
// ...
}
複数のインスタンスを持つキャッシュされたノード
複数のインスタンスを持つ可能性のある特殊化があるノードの場合、インライン・ターゲット・ノードにアクセスするには、@Bind("this") Node node
パラメータを使用する必要があります。これは、高度な使用例のSumArrayNode
ノードに似ています。
@ImportStatic(AbstractArray.class)
public abstract static class SumArrayNode extends Node {
abstract int execute(Object v0);
@Specialization(guards = {"kind != null", "kind.type == array.getClass()"}, limit = "2", unroll = 2)
static int doDefault(Object array,
@Bind("this") Node node,
@Cached("resolve(array)") ArrayKind kind,
@Cached GetStoreNode getStore) {
Object castStore = kind.type.cast(array);
int[] store = getStore.execute(node, castStore);
int sum = 0;
for (int element : store) {
sum += element;
TruffleSafepoint.poll(node);
}
return sum;
}
static Class<?> getCachedClass(Object array) {
if (array instanceof AbstractArray) {
return array.getClass();
}
return null;
}
}
エクスポートされたライブラリ・メッセージ
エクスポートされたライブラリ・メッセージの場合、this
キーワードはすでにレシーバ値用に予約されているため、かわりに$node
を使用できます。
たとえば:
@ExportLibrary(ExampleArithmeticLibrary.class)
static class ExampleNumber {
final long value;
/* ... */
@ExportMessage
final long abs(@Bind("$node") Node node,
@Cached InlinedConditionProfile profile) {
if (profile.profile(node, this.value >= 0)) {
return this.value;
} else {
return -this.value;
}
}
}
制限事項
ノード・オブジェクトのインライン化は、任意の深いネストをサポートしています。ただし、@GenerateInline
の使用にはいくつかの制限があります。
- ノード・クラスまたは親クラスにインスタンス・フィールドを設定することはできません。
- ノードには
@NodeField
または@NodeChild
を使用しないでください。 - インライン化されたノードの使用は、再帰的であってはなりません。
インライン化可能なノードおよびプロファイルの手動実装
DSLでインライン化できるノードまたはプロファイルは、手動で実装することもできます。クラスは、inline
という静的メソッドを実装する必要があります。たとえば、ほとんどのインライン化可能なTruffleプロファイルでは、カスタム・インライン化が使用されます。このようなインライン化可能なクラスを実装する場合は、特に注意が必要です。可能であれば、かわりにDSLで生成されたノードを使用してください。インライン・メソッドの実装方法の例は、InlinedBranchProfileまたはInlinedIntValueProfileクラスを参照してください。
インライン化可能なノードのAPI互換性
TruffleString
APIは、前述の例のようにDSLノードを幅広く使用します。ただし、ノードをインライン化できるようにすると、そのノードの特殊化に対するすべての変更が互換性のないAPIの変更になります。これは、静的inline
メソッドのシグネチャが、特殊化の必要な状態ビットに応じて変わるためです。
安定したAPIの境界を越えてインライン化をサポートするには、生成されたインライン・メソッドに転送するインライン・メソッドを手動で指定することをお薦めします。
例として、次のノードを考えてみます:
@GenerateInline
@GenerateUncached
@GeneratePackagePrivate
public abstract static class APINode extends Node {
abstract long execute(Node node, long value);
@Specialization(guards = "v >= 0")
static long doInt(long v) {
return v;
}
@Specialization(guards = "v < 0")
static long doLong(long v) {
return -v;
}
public static APINode inline(@RequiredField(value = StateField.class, bits = 32) InlineContext context) {
return APINodeGen.inline(context);
}
public static APINode create() {
return APINodeGen.create();
}
public static APINode getUncached() {
return APINodeGen.getUncached();
}
}
生成されたコードをパブリックとして公開しないようにするには、@GeneratePackagePrivate
を使用します。このノードに必要なビットを指定する手動のinline
メソッドを指定します。ノードの特殊化で、指定より多くのビット数、または指定以外の追加のフィールドが必要な場合、注釈プロセッサはエラーで失敗します。ノードが必要とするビット数が少ない場合、コンパイラ・エラーは発生しません。これにより、予約済のフィールド容量を超えないかぎり、APIは安定したAPIの境界を越えてノードのインライン化を使用できます。
次の場合、変更は互換性があります:
- 以前にこのノードの
inline
メソッドがなかった。 - 必要なビット領域が削減され、他のすべてのフィールドが変更された場合。
次の場合、変更は互換性がありません:
- 既存の
inline
メソッドへの新しい@RequiredField
注釈が追加または削除された。 - 必要なビット数が増えた。
DSLは、必須フィールドが親ノードの状態仕様と一致しているかどうかを検証し、ノード仕様と互換性がない場合は警告を発行します。
DSLインライン化を使用した遅延初期化ノード
この例の完全なソース・コード: NodeInliningAndLazyInitExample.java。
DSLインライン化を使用すると、めったにトリガーされない条件によって保護されているコード・ブロックでのみ使用されるキャッシュされたノードに対して遅延初期化を実行できます。次の例を考えてみましょう。
@GenerateInline(false)
@GenerateUncached
public abstract static class RaiseErrorNode extends Node {
abstract void execute(Object type, String message);
// ...
}
@GenerateInline(false)
@GenerateUncached(false)
public abstract static class LazyInitExampleBefore extends Node {
abstract void execute(Object value);
@Specialization
void doIt(Object value,
@Cached RaiseErrorNode raiseError) {
Object result = doSomeWork(value);
if (result == null) {
raiseError.execute(value, "Error: doSomeWork returned null");
}
}
}
doSomeWork
が実行時に常にnull
以外の結果を返す場合、RaiseErrorNode
は、必要ない場合でも常にインスタンス化されます。DSLインライン化の前は、この問題は通常、遅延初期化された@Child
ノードによって解決されました。
@GenerateInline(false)
@GenerateUncached(false)
public abstract static class LazyInitExampleBefore2 extends Node {
@Child RaiseErrorNode raiseError;
abstract void execute(Object value);
@Specialization
void doIt(Object value) {
Object result = doSomeWork(value);
if (result == null) {
if (raiseError == null) {
CompilerDirectives.transferToInterpreterAndInvalidate();
raiseError = insert(RaiseErrorNodeGen.create());
}
raiseError.execute(value, "Error: doSomeWork returned null");
}
}
}
ただし、@Child
ノードにはいくつかの欠点があります。特に注目すべきは、@Specialization
をstatic
にすることはできず、ノードのキャッシュされていないバリアントを生成できないことです。
DSLインライン化では、有益であればRaiseErrorNode
をインライン化可能にする必要があります。または次のようなノードの場合:
- 複数のインスタンスを持つ多くの特殊化がある、または
- 現在インライン化できない、または
- インライン化できないキャッシュされたフィールドが多い
その場合は、必要に応じてRaiseErrorNode
を初期化するインライン化可能なラッパー・ノードを作成できます。
@GenerateInline
@GenerateUncached
@GenerateCached(false)
public abstract static class LazyRaiseNode extends Node {
public final RaiseErrorNode get(Node node) {
return execute(node);
}
abstract RaiseErrorNode execute(Node node);
@Specialization
static RaiseErrorNode doIt(@Cached(inline = false) RaiseErrorNode node) {
return node;
}
}
@GenerateInline(false)
@GenerateUncached
public abstract static class LazyInitExample extends Node {
abstract void execute(Object value);
@Specialization
void doIt(Object value,
@Cached LazyRaiseNode raiseError) {
Object result = doSomeWork(value);
if (result == null) {
raiseError.get(this).execute(value, "Error: doSomeWork returned null");
}
}
}
LazyRaiseNode.execute
がコールされないかぎり、ラッパーのコストは単一の参照フィールドであり、LazyInitExample
ノードのビットセットからの1ビットです。余分なビットを除き、遅延初期化された@Child
ノード・フィールドと同じです。