Truffleライブラリ・ガイド

Truffleライブラリを使用すると、言語実装でレシーバの型に多相ディスパッチを使用でき、実装固有のキャッシング/プロファイリングのサポートおよびキャッシュされていないディスパッチの自動サポートが提供されます。Truffleライブラリにより、Truffle上の表現型の言語実装のモジュール性およびカプセル化が可能になります。使用する前に、まずこのガイドをお読みください。

スタート・ガイド

このチュートリアルでは、Truffleライブラリの使用方法に関するユースケース全体のトレースを示します。完全なAPIドキュメントはJavadocを参照してください。このドキュメントは、Truffle API、および@Cached注釈を使用した@Specializationの使用の予備知識を前提としています。

使用する理由の例

Truffle言語で配列を実装するとき、多くの場合、効率を上げるために複数の表現を使用する必要があります。たとえば、配列が整数の等差数列(range(from: 1, step: 2, length: 3)など)で構成されている場合、配列全体をマテリアライズするかわりに、startstrideおよびlengthを使用して最適に表現されます。当然ながら、配列要素が記述されるときは、配列をマテリアライズする必要があります。この例では、次の2つの表現を使用して配列実装を実装します:

例を簡潔にするために、int値のみをサポートし、インデックス境界エラー処理は無視します。また、読取り操作のみを実装し、通常はより複雑な書込み操作は実装しません。

例をさらに興味深いものにするために、配列レシーバの値が定数でない場合でも、コンパイラが定数畳込みが順序付けされた配列アクセスを許可できるようにする最適化を実装します。

次のコード・スニペットrange(start, stride, length)[2]があるとします。このスニペットでは、変数startおよびstrideが定数値であるとわからないため、start + stride * 2と同等のコードがコンパイルされます。ただし、startおよびstrideの値が常に同じであることがわかっている場合、コンパイラはその操作全体を定数畳込みできます。この最適化にはキャッシュを使用する必要があります。この仕組みについては、後で説明します。

GraalVMのJavaScriptランタイムの動的配列実装では、20の異なる表現を使用します。定数、ゼロベース、連続、ホールおよびスパース配列の表現があります。一部の表現は、さらにbyteintdoubleJSObjectおよびObject型に特殊化されています。ソース・コードはこちらを参照してください。ノート: 現在、JavaScript配列ではTruffleライブラリをまだ使用していません。

次の項では、配列表現の複数の実装戦略について説明し、最終的にはTruffleライブラリを使用してこれを実現する方法について説明します。

戦略1: 表現ごとの特殊化

この戦略では、まずBufferArrayおよびSequenceArrayという2つの表現のクラスを宣言します。

final class BufferArray {
    int length;
    int[] buffer;
    /*...*/
}

final class SequenceArray {
    final int start;
    final int stride;
    final int length;
    /*...*/
}

BufferArray実装には可変のバッファおよび長さがあり、マテリアライズされた配列表現として使用されます。シーケンス配列は、最後のフィールドstartstrideおよびlengthで表されます。

ここで、次のような基本的な読取り操作を指定します:

abstract class ExpressionNode extends Node {
    abstract Object execute(VirtualFrame frame);
}

@NodeChild @NodeChild
abstract class ArrayReadNode extends ExpressionNode {

    @Specialization
    int doBuffer(BufferArray array, int index) {
        return array.buffer[index];
    }

    @Specialization
    int doSequence(SequenceArray seq, int index) {
        return seq.start + seq.stride * index;
    }
}

配列読取りノードでは、バッファ・バージョンおよびシーケンスに対して2つの特殊化を指定します。前述のとおり、簡略化のためにエラー境界チェックを無視します。

ここで、配列読取りをシーケンスの値の定数性に特殊化して、startおよびstrideが定数の場合にrange(start, stride, length)[2]の例を畳込みできるようにします。startおよびstrideが定数かどうかを調べるには、それらの値をプロファイルする必要があります。これらの値をプロファイルするには、次のように配列読取り操作に別の特殊化を追加する必要があります:

@NodeChild @NodeChild
class ArrayReadNode extends ExpressionNode {
    /* doBuffer() */
    @Specialization(guards = {"seq.stride == cachedStride",
                              "seq.start  == cachedStart"}, limit = "1")
    int doSequenceCached(SequenceArray seq, int index,
             @Cached("seq.start")  int cachedStart,
             @Cached("seq.stride") int cachedStride) {
        return cachedStart + cachedStride * index;
    }
    /* doSequence() */
}

この特殊化の推測ガードが成功した場合、startおよびstrideは事実上定数です。たとえば、値が3および2の場合、コンパイラには3 + 2 * 2 (7)が表示されます。この推測を1回のみ試行するには、制限を1に設定します。これにより、コンパイルされたコードに追加の制御フローが導入されるため、制限を増やすと非効率になる可能性があります。推測が成功しなかった場合、つまり操作で複数のstartおよびstrideの値が観察される場合は、通常のシーケンス特殊化にフォールバックします。これを実現するには、次のようにreplaces = "doSequenceCached"を追加して、doSequence特殊化を変更します:

@NodeChild @NodeChild
class ArrayReadNode extends ExpressionNode {
    /* doSequenceCached() */
    @Specialization(replaces = "doSequenceCached")
    int doSequence(SequenceArray seq, int index) {
        return seq.start + seq.stride * index;
    }
}

これで、追加のプロファイリングを含む配列表現を実装するという目標を達成しました。戦略1の実行可能ソース・コードは、こちらを参照してください。この戦略には、次のような優れた特性があります:

これに問題がなければ、このチュートリアルはすでに完了しています:

これらの問題は、Truffleライブラリを使用する主な理由となります。

戦略2: Javaインタフェース

ここでは、Javaインタフェースを使用してこれらの問題への対処を試みます。まず、配列インタフェースを定義します:

interface Array {
    int read(int index);
}

これで、これらの実装でArrayインタフェースを実装し、表現クラスに読取りメソッドを実装できるようになりました。

final class BufferArray implements Array {
    private int length;
    private int[] buffer;
    /*...*/
    @Override public int read(int index) {
        return buffer[index];
    }
}

final class SequenceArray implements Array {
    private final int start;
    private final int stride;
    private final int length;
    /*...*/
    @Override public int read(int index) {
        return start + (stride * index);
    }
}

最後に、操作ノードを指定します:

@NodeChild @NodeChild
abstract class ArrayReadNode extends ExpressionNode {
    @Specialization
   int doDefault(Array array, int index) {
        return array.read(index);
    }
}

この操作の実装の問題は、部分エバリュエータが配列レシーバの具象型を認識していないことです。したがって、部分評価を停止し、readメソッド・コールに対してスロー・インタフェース・コールを発行する必要があります。これは必要なものではありませんが、次のように多相型キャッシュを導入して解決できます:

class ArrayReadNode extends ExpressionNode {
    @Specialization(guards = "array.getClass() == arrayClass", limit = "2")
    int doCached(Array array, int index,
           @Cached("array.getClass()") Class<? extends Array> arrayClass) {
        return arrayClass.cast(array).read(index);
    }

    @Specialization(replaces = "doCached")
    int doDefault(Array array, int index) {
        return array.read(index);
    }
}

実装を部分的に評価するという問題は解決しましたが、このソリューションでは、定数strideおよびstartのインデックス最適化のために追加の特殊化を表現する方法はありません。

これまでにわかったこと/解決されたことを次に示します:

ただし、次の新しい問題が発生しました:

戦略2の実行可能ソース・コードは、こちらを参照してください。

戦略3: Truffleライブラリ

Truffleライブラリは、Javaインタフェースと同様に機能します。Javaインタフェースのかわりに、Libraryクラスを拡張する抽象クラスを作成し、@GenerateLibraryで注釈を付けます。インタフェースと同様に抽象メソッドを作成しますが、最初にレシーバ引数を挿入します(Object型の場合)。インタフェース型チェックを実行するかわりに、通常はis${Type}という名前のライブラリで明示的な抽象メソッドを使用します。

この例では、次を実行します:

@GenerateLibrary
public abstract class ArrayLibrary extends Library {

    public boolean isArray(Object receiver) {
        return false;
    }

    public abstract int read(Object receiver, int index);
}

このArrayLibraryは、isArrayおよびreadの2つのメッセージを指定します。コンパイル時に、注釈プロセッサはパッケージで保護されたクラスArrayLibraryGenを生成します。生成されるノード・クラスとは異なり、このクラスを参照する必要はありません。

Javaインタフェースを実装するかわりに、表現型で@ExportLibrary注釈を使用してライブラリをエクスポートします。メッセージのエクスポートは、表現でインスタンス・メソッドを使用して指定されるため、ライブラリのレシーバ引数を省略できます。

この方法で実装する最初の表現は、BufferArray表現です:

@ExportLibrary(ArrayLibrary.class)
final class BufferArray {
    private int length;
    private int[] buffer;
    /*...*/
    @ExportMessage boolean isArray() {
      return true;
    }
    @ExportMessage int read(int index) {
      return buffer[index];
    }
}

この実装はインタフェース・バージョンと非常に似ていますが、さらにisArrayメッセージを指定します。この場合も、注釈プロセッサは、ライブラリ抽象クラスを実装するボイラープレート・コードを生成します。

次に、シーケンス表現を実装します。まず、startおよびstride値を最適化せずに実装します。

@ExportLibrary(ArrayLibrary.class)
final class SequenceArray {
    private final int start;
    private final int stride;
    private final int length;
    /*...*/
    @ExportMessage int read(int index) {
        return start + stride * index;
    }
}

これまでのところ、これはインタフェースの実装と同等でしたが、Truffleライブラリでは、メソッドではなくクラスを使用してメッセージをエクスポートすることで、表現に特殊化を使用することもできます。規則では、クラスにはエクスポートされるメッセージとまったく同じ名前が付けられますが、最初の文字は大文字になります。

次に、このメカニズムを使用して、strideおよびstart特殊化を実装します:

@ExportLibrary(ArrayLibrary.class)
final class SequenceArray {
    final int start;
    final int stride;
    final int length;
    /*...*/

    @ExportMessage static class Read {
        @Specialization(guards = {"seq.stride == cachedStride",
                                  "seq.start  == cachedStart"}, limit = "1")
        static int doSequenceCached(SequenceArray seq, int index,
                 @Cached("seq.start")  int cachedStart,
                 @Cached("seq.stride") int cachedStride) {
            return cachedStart + cachedStride * index;
        }

        @Specialization(replaces = "doSequenceCached")
        static int doSequence(SequenceArray seq, int index) {
            return doSequenceCached(seq, index, seq.start, seq.stride);
        }
    }
}

メッセージは内部クラスを使用して宣言されるため、レシーバの型を指定する必要があります。通常のノードと比較して、このクラスはNodeを拡張できず、注釈プロセッサがライブラリ・サブクラスの効率的なコードを生成できるように、そのメソッドはstaticである必要があります。

最後に、読取り操作で配列ライブラリを使用する必要があります。ライブラリAPIは、ライブラリへのディスパッチを担当する@CachedLibraryという注釈を提供します。配列読取り操作は次のようになります:

@NodeChild @NodeChild
class ArrayReadNode extends ExpressionNode {
    @Specialization(guards = "arrays.isArray(array)", limit = "2")
    int doDefault(Object array, int index,
                  @CachedLibrary("array") ArrayLibrary arrays) {
        return arrays.read(array, index);
    }
}

戦略2で示した型キャッシュと同様に、ライブラリを特定の値に特殊化します。@CachedLibraryの最初の属性"array"は、ライブラリが特殊化される値を指定します。特殊化されたライブラリは、特殊化された値にのみ使用できます。これらを他の値とともに使用すると、フレームワークはアサーション・エラーで失敗します。

Array型をパラメータ型として使用するかわりに、ガードでisArrayメッセージを使用します。特殊化されたライブラリを使用するには、特殊化の制限を指定する必要があります。この制限では、キャッシュされていないバージョンのライブラリを使用するように操作が自身を書き換えるまでインスタンス化できるライブラリの特殊化の数を指定します。

配列の例では、2つの配列表現のみを実装しました。そのため、この制限を超えることはできません。実際の配列の実装では、さらに多くの表現を使用する可能性があります。この制限は、代表的なアプリケーションで超える可能性が低い値に設定する必要がありますが、同時に、コードが過剰に生成されることはありません。

キャッシュされていないバージョンまたはスローパス・バージョンのライブラリには、特殊化の制限を超えることで到達できますが、使用可能なノードがないときに配列操作を呼び出す必要がある場合などは、手動で使用することもできます。これは通常、頻繁に呼び出されない言語実装の部分に当てはまります。インタフェース戦略(戦略2)では、単にインタフェース・メソッドを呼び出すことで配列読取り操作を使用できます。

Truffleライブラリでは、まず、キャッシュされていないバージョンのライブラリをルックアップする必要があります。@ExportLibraryを使用するたびに、キャッシュされているライブラリ・サブクラスに加え、キャッシュされていない/スローパス・ライブラリ・サブクラスも生成されます。エクスポートされたライブラリのキャッシュされていないバージョンでは、@GenerateUncachedと同じセマンティクスが使用されます。通常、この例と同様に、キャッシュされていないバージョンを自動的に導出できます。DSLでは、キャッシュされていないバージョンの生成方法の詳細が必要な場合、エラーが表示されます。キャッシュされていないバージョンのライブラリは、次のように呼び出すことができます:

ArrayLibrary arrays = LibraryFactory.resolve(ArrayLibrary.class).getUncached();
arrays.read(array, index);

この例の冗長性を減らすには、ライブラリ・クラスで次のオプションの静的ユーティリティを指定することをお薦めします:

@GenerateLibrary
public abstract class ArrayLibrary extends Library {
    /*...*/
    public static LibraryFactory<ArrayLibrary> getFactory() {
        return FACTORY;
    }

    public static ArrayLibrary getUncached() {
        return FACTORY.getUncached();
    }

    private static final LibraryFactory<ArrayLibrary> FACTORY =
               LibraryFactory.resolve(ArrayLibrary.class);
}

前述の冗長な例は、次のように単純化できます:

ArrayLibrary.getUncached().readArray(array, index);

戦略3の実行可能ソース・コードは、こちらを参照してください。

まとめ

このチュートリアルでは、Truffleライブラリを使用して、表現ごとに特殊化を作成することで表現型のモジュール性を犠牲にする必要がなくなり(戦略1)、プロファイリングがインタフェース・コールによってブロックされなくなった(戦略2)ことを学習しました。Truffleライブラリでは、型のカプセル化による多相ディスパッチがサポートされるようになりましたが、表現型でプロファイリング/キャッシング手法が使用不可になることはありません。

次の作業

FAQ

既知の制限はありますか。

Truffleライブラリはどのような場合に使用する必要がありますか。

使用する状況

使用しない状況

Truffleライブラリを使用して言語の言語固有の型を抽象化することにしました。それらを他の言語およびツールに公開する必要はありますか。

すべてのライブラリは、ReflectionLibraryを介して他の言語およびツールにアクセスできます。言語実装ドキュメントでは、外部使用を目的としたライブラリおよびメッセージと、破壊的変更の影響を受ける可能性があるライブラリおよびメッセージを指定することをお薦めします。

新しいメソッドがライブラリに追加されたが、動的にロードされた実装がそれに対して更新されていない場合はどうなりますか。

ライブラリ・メソッドにabstractが指定されている場合は、AbstractMethodErrorがスローされます。それ以外の場合は、ライブラリ・メソッド本体で指定されたデフォルトの実装がコールされます。これにより、抽象メソッドが使用された場合のエラーをカスタマイズできます。たとえば、Truffleの相互運用性の場合、AbstractMethodErrorではなくUnsupportedMessageExceptionがスローされることがよくあります。