- 適用於 JDK 23 的 GraalVM (最新)
- 適用於 JDK 24 的 GraalVM (搶先體驗)
- 適用於 JDK 21 的 GraalVM
- 適用於 JDK 17 的 GraalVM
- 封存
- 開發版本
- Truffle 語言實作框架
- Truffle 分支檢測
- 動態物件模型
- 靜態物件模型
- 直譯器程式碼的主機最佳化
- Truffle 的函式內聯方法
- 剖析 Truffle 直譯器
- Truffle Interop 2.0
- 語言實作
- 使用 Truffle 實作新語言
- Truffle 語言和工具遷移至 Java 模組
- Truffle 原生函式介面
- 最佳化 Truffle 直譯器
- 選項
- 堆疊上替換
- Truffle 字串指南
- 特殊化長條圖
- 測試 DSL 特殊化
- 基於 Polyglot API 的 TCK
- Truffle 的編譯佇列方法
- Truffle 程式庫指南
- Truffle AOT 概觀
- Truffle AOT 編譯
- 輔助引擎快取
- Truffle 語言安全點教學課程
- 單態化
- 分割演算法
- 單態化使用案例
- 向執行階段報告多型特殊化
Truffle 程式庫指南
Truffle 程式庫允許語言實作針對接收器類型使用多型分派,並支援特定實作的快取/剖析以及自動支援未快取的分派。Truffle 程式庫為 Truffle 之上的語言實作的表示類型啟用模組化和封裝。在使用它們之前,請先閱讀本指南。
開始使用 #
本教學課程提供如何使用 Truffle 程式庫的使用案例追蹤。完整的 API 文件可在 Javadoc 中找到。本文檔假設您事先瞭解 Truffle API 以及 @Specialization
與 @Cached
註釋的使用。
動機範例 #
在 Truffle 語言中實作陣列時,通常需要使用多種表示形式以提高效率。例如,如果陣列是從整數的算術序列建構而成的 (例如,range(from: 1, step: 2, length: 3)
),則最好使用 start
、stride
和 length
來表示,而不是實體化整個陣列。當然,當寫入陣列元素時,就需要實體化陣列。在本範例中,我們將使用兩種表示形式實作陣列實作
- Buffer:表示由 Java 陣列支援的已實體化的陣列表示形式。
- Sequence:表示由
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
的類別。
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;
}
}
陣列讀取節點指定緩衝區版本和序列的兩種特殊化。如前所述,為了簡單起見,我們將忽略錯誤界限檢查。
現在,我們嘗試讓陣列讀取針對序列值的常數性進行特殊化,以便在開始和跨距是常數時,允許摺疊 range(start, stride, length)[2]
範例。為了找出開始和跨距是否為常數,我們需要剖析它們的值。為了剖析這些值,我們需要在陣列讀取操作中新增另一個特殊化,如下所示
@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() */
}
如果此特殊化的推測保護成功,則開始和跨距實際上是常數。例如,使用值 3
和 2
,編譯器會看到 3 + 2 * 2
,即 7
。限制設定為 1
,僅嘗試推測一次。增加限制可能會沒有效率,因為這會向已編譯的程式碼引入其他控制流程。如果推測不成功,也就是說,如果操作觀察到多個開始和跨距值,我們希望退回到正常的序列特殊化。為了達成此目的,我們變更 doSequence
特殊化,新增 replaces = "doSequenceCached"
,如下所示
@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);
}
}
我們解決了部分評估實作的問題,但在這個解決方案中,無法表示常數跨距和起始索引最佳化的額外特殊化。
這就是我們目前發現/解決的
- 介面是 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
。在編譯時,註釋處理器會產生受套件保護的類別 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
訊息。同樣地,註釋處理器會產生實作程式庫抽象類別的樣板程式碼。
接下來,我們實作序列表示形式。首先,我們實作它,而不對開始和跨距值進行最佳化。
@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 程式庫,我們現在也可以在表示形式中使用特殊化,方法是使用類別而不是方法來匯出訊息。慣例是類別的名稱與匯出的訊息完全相同,但第一個字母為大寫。
現在,我們使用此機制實作我們的跨距和開始特殊化
@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),只需調用介面方法即可使用陣列讀取操作。
使用 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 函式庫,我們現在支援具有類型封裝的多型派送,但不會失去在表示法類型中使用分析/快取技術的能力。
下一步該做什麼? #
常見問題解答 #
是否有任何已知的限制?
- 函式庫匯出目前無法明確調用其
super
實作。這使得反射實作目前不可行。請參閱此處的範例這裡。 - 目前不支援傳回值的 Boxing 消除。一個訊息只能有一個泛型傳回類型。計畫支援此功能。
- 目前不支援在沒有對
Library
類別的靜態相依性的情況下進行反射。計畫支援完整的動態反射。
我應該在什麼時候使用 Truffle 函式庫?
何時使用?
- 如果表示法是模組化的,且無法針對某個操作進行枚舉(例如,Truffle 互通性)。
- 如果一個類型有多個表示法,且其中一個表示法需要分析/快取(例如,請參閱動機範例)。
- 如果需要一種代理語言所有值的方式(例如,用於動態污點追蹤)。
何時不使用?
- 對於只有一個表示法的基本類型。
- 對於需要 Boxing 消除以加速直譯器的基本表示法。Truffle 函式庫目前不支援 Boxing 消除。
我決定使用 Truffle 函式庫來抽象化我的語言的特定類型。這些類型應該對其他語言和工具公開嗎?
所有函式庫都可以透過 ReflectionLibrary
供其他語言和工具存取。建議語言實作文件指定哪些函式庫和訊息打算供外部使用,以及哪些可能會發生重大變更。
當一個新的方法被添加到函式庫中,但動態載入的實作尚未更新時會發生什麼事?
如果函式庫方法被指定為 abstract
,則會拋出 AbstractMethodError
。否則,將呼叫函式庫方法主體指定的預設實作。這允許在使用了抽象方法時自訂錯誤。例如,對於 Truffle 互通性,我們通常會拋出 UnsupportedMessageException
而不是 AbstractMethodError
。