Truffle DSLノード・オブジェクトのインライン化

23.0では、@GenerateInlineという新しい注釈が導入されました。この注釈は、Truffle DSL注釈プロセッサに、ノードのインライン化可能なバージョンを生成するように指示します。これは、キャッシュされたノード・バージョンまたはキャッシュされていないノード・バージョンを生成する@GenerateCachedおよび@GenerateUncachedと同様に機能します。デフォルトでは、DSLはノードのインライン化されたバージョンを生成しません。ノードのインライン化は、ノードのメモリー・フットプリントを削減する簡単な方法を提供しますが、多くの場合、インタプリタの実行速度も向上させます。

基本的な使用方法

2つの値の絶対値の合計を計算する特殊化を持つノードがあるとします。わかりやすくするために、この例ではlong型の特殊化のみを見ていきます。

この例の実行可能だが少し高度なバージョンは、Truffleユニット・テストにあります。

特殊化する2つの通常のノードを指定する次の例を考えてみます。1つのノードは2つの値の合計を計算し、もう1つのノードは数値の絶対数を計算します。AbsNodeAddAbsNodeで再利用され、実装を共有します。

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);
    }
    // ...
}

これで、AbsNodeAddAbsNodeにオブジェクト・インライン化できました。新しいメモリー・フットプリントは次のように計算されます:

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)を明示的に指定することで警告を抑制しました。

インライン・キャッシュの高度な使用方法

次の例では、特殊化のアンロールおよび新しいインライン化可能なキャッシュ・クラスが、複数のインスタンスを持つ特殊化を使用して、ノードのメモリー・フットプリントを削減する際にどのように役立つかを説明します。

例:

ノードの適切な受渡し

インライン化されたノードを使用するには、それぞれのインライン化されたノードのメソッドを実行するために、正しいノードにアクセスして渡す必要があります。メソッドを実行する際に誤ったノードを渡してしまうのは、よくある間違いです。通常、このような誤りは実行時にエラーで失敗しますが、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の使用にはいくつかの制限があります。

インライン化可能なノードおよびプロファイルの手動実装

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の境界を越えてノードのインライン化を使用できます。

次の場合、変更は互換性があります:

次の場合、変更は互換性がありません:

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ノードにはいくつかの欠点があります。特に注目すべきは、@Specializationstaticにすることはできず、ノードのキャッシュされていないバリアントを生成できないことです。

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ノード・フィールドと同じです。