Truffleライブラリ・ガイド
Truffleライブラリを使用すると、言語実装でレシーバの型に多相ディスパッチを使用でき、実装固有のキャッシング/プロファイリングのサポートおよびキャッシュされていないディスパッチの自動サポートが提供されます。Truffleライブラリにより、Truffle上の表現型の言語実装のモジュール性およびカプセル化が可能になります。使用する前に、まずこのガイドをお読みください。
スタート・ガイド
このチュートリアルでは、Truffleライブラリの使用方法に関するユースケース全体のトレースを示します。完全なAPIドキュメントはJavadocを参照してください。このドキュメントは、Truffle API、および@Cached
注釈を使用した@Specialization
の使用の予備知識を前提としています。
使用する理由の例
Truffle言語で配列を実装するとき、多くの場合、効率を上げるために複数の表現を使用する必要があります。たとえば、配列が整数の等差数列(range(from: 1, step: 2, length: 3)
など)で構成されている場合、配列全体をマテリアライズするかわりに、start
、stride
およびlength
を使用して最適に表現されます。当然ながら、配列要素が記述されるときは、配列をマテリアライズする必要があります。この例では、次の2つの表現を使用して配列実装を実装します:
- バッファ: Java配列でサポートされるマテリアライズされた配列表現を表します。
- シーケンス:
start
、stride
およびlength
で表される数値の等差数列を表します:[start, start + 1 * stride, ..., start + (length - 1) * stride]
。
例を簡潔にするために、int
値のみをサポートし、インデックス境界エラー処理は無視します。また、読取り操作のみを実装し、通常はより複雑な書込み操作は実装しません。
例をさらに興味深いものにするために、配列レシーバの値が定数でない場合でも、コンパイラが定数畳込みが順序付けされた配列アクセスを許可できるようにする最適化を実装します。
次のコード・スニペットrange(start, stride, length)[2]
があるとします。このスニペットでは、変数start
およびstride
が定数値であるとわからないため、start + stride * 2
と同等のコードがコンパイルされます。ただし、start
およびstride
の値が常に同じであることがわかっている場合、コンパイラはその操作全体を定数畳込みできます。この最適化にはキャッシュを使用する必要があります。この仕組みについては、後で説明します。
GraalVMのJavaScriptランタイムの動的配列実装では、20の異なる表現を使用します。定数、ゼロベース、連続、ホールおよびスパース配列の表現があります。一部の表現は、さらにbyte
、int
、double
、JSObject
および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
実装には可変のバッファおよび長さがあり、マテリアライズされた配列表現として使用されます。シーケンス配列は、最後のフィールドstart
、stride
および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の実行可能ソース・コードは、こちらを参照してください。この戦略には、次のような優れた特性があります:
- この操作は読みやすく、すべてのケースが完全に列挙されています。
- 読取りノードの生成されたコードでは、実行時に観察された表現型を記憶するために、特殊化ごとに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のインデックス最適化のために追加の特殊化を表現する方法はありません。
これまでにわかったこと/解決されたことを次に示します:
- インタフェースは、Javaにおける多相性の既存の既知の概念です。
- 新しいインタフェースの実装をロードしてモジュール性を可能にできます。
- スローパスから操作を使用する便利な方法を見つけました。
- 表現型は、実装の詳細をカプセル化できます。
ただし、次の新しい問題が発生しました:
- 表現固有のプロファイリング/キャッシングは実行できません。
- すべてのインタフェース・コールには、コール・サイトの多相クラス・キャッシュが必要です。
戦略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ライブラリでは、型のカプセル化による多相ディスパッチがサポートされるようになりましたが、表現型でプロファイリング/キャッシング手法が使用不可になることはありません。
次の作業
-
こちらのすべての例を実行してデバッグします。
-
こちらでTruffleライブラリの使用例として、相互運用性移行ガイドをお読みください。
-
こちらでTruffleライブラリのリファレンス・ドキュメントをお読みください。
FAQ
既知の制限はありますか。
- ライブラリのエクスポートは現在、
super
実装を明示的に呼び出すことはできません。これにより、リフレクティブな実装は現在実行不可能です。こちらの例を参照してください。 - 戻り値のボックス化の排除は現在サポートされていません。メッセージには、一般的な戻り型を1つのみ含めることができます。これはサポートされる予定です。
Library
クラスへの静的な依存性がないリフレクションは現在サポートされていません。完全な動的リフレクションはサポートされる予定です。
Truffleライブラリはどのような場合に使用する必要がありますか。
使用する状況
- 表現がモジュール型で、操作に対して列挙できない場合(Truffleの相互運用性など)。
- 型の表現が複数あり、いずれかの表現にプロファイリング/キャッシングが必要な場合(使用する理由の例を参照)。
- 言語のすべての値をプロキシする方法が必要な場合(動的汚染トラッキングの場合など)。
使用しない状況
- 表現が1つのみの基本型の場合。
- インタプリタを高速化するためにボックス化の排除を必要とするプリミティブ表現の場合。現在、Truffleライブラリではボックス化の排除はサポートされていません。
Truffleライブラリを使用して言語の言語固有の型を抽象化することにしました。それらを他の言語およびツールに公開する必要はありますか。
すべてのライブラリは、ReflectionLibrary
を介して他の言語およびツールにアクセスできます。言語実装ドキュメントでは、外部使用を目的としたライブラリおよびメッセージと、破壊的変更の影響を受ける可能性があるライブラリおよびメッセージを指定することをお薦めします。
新しいメソッドがライブラリに追加されたが、動的にロードされた実装がそれに対して更新されていない場合はどうなりますか。
ライブラリ・メソッドにabstract
が指定されている場合は、AbstractMethodError
がスローされます。それ以外の場合は、ライブラリ・メソッド本体で指定されたデフォルトの実装がコールされます。これにより、抽象メソッドが使用された場合のエラーをカスタマイズできます。たとえば、Truffleの相互運用性の場合、AbstractMethodError
ではなくUnsupportedMessageException
がスローされることがよくあります。