向執行期回報多型特化

本指南概述了語言實作者為了利用單態化(分割)策略所需的事項。關於其運作方式的更多資訊,請參閱分割指南。

簡單來說,單態化啟發法依賴於語言回報每個可能透過分割回到單態狀態的節點的多型特化。在此情境中,多型特化是指任何節點重寫導致節點改變其「多型程度」的狀況。這包括但不限於:啟用另一個特化、增加作用中特化的實例數量、排除特化等。

手動回報多型特化 #

為了方便回報多型特化,在 Node 類別中引入了一個新的 API:Node#reportPolymorphicSpecialize。此方法可用於手動回報多型特化,但僅限於無法透過使用 DSL 自動化的情況。

自動回報多型特化 #

由於 Truffle DSL 自動化了特化之間的大部分轉換,因此新增了用於自動回報多型特化的 @ReportPolymorphism 註解。此註解指示 DSL 在特化後包含多型檢查,並在需要時呼叫 Node#reportPolymorphicSpecialize

有關如何使用此註解的範例,請考慮 com.oracle.truffle.sl.nodes.SLStatementNode。它是所有 SimpleLanguage 節點的基底類別,由於 ReportPolymorphism 註解是繼承的,因此僅註解此類別即可為所有 SimpleLanguage 節點啟用多型特化的回報。以下是將此註解新增至 SLStatementNode 的變更差異。

diff --git
a/truffle/src/com.oracle.truffle.sl/src/com/oracle/truffle/sl/nodes/SLStatementNode.java
b/truffle/src/com.oracle.truffle.sl/src/com/oracle/truffle/sl/nodes/SLStatementNode.java
index 788cc20..89448b2 100644
---
a/truffle/src/com.oracle.truffle.sl/src/com/oracle/truffle/sl/nodes/SLStatementNode.java
+++
b/truffle/src/com.oracle.truffle.sl/src/com/oracle/truffle/sl/nodes/SLStatementNode.java
@@ -43,6 +43,7 @@ package com.oracle.truffle.sl.nodes;
 import java.io.File;

 import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
+import com.oracle.truffle.api.dsl.ReportPolymorphism;
 import com.oracle.truffle.api.frame.VirtualFrame;
 import com.oracle.truffle.api.instrumentation.GenerateWrapper;
 import com.oracle.truffle.api.instrumentation.InstrumentableNode;
@@ -62,6 +63,7 @@ import com.oracle.truffle.api.source.SourceSection;
  */
 @NodeInfo(language = "SL", description = "The abstract base node for all SL
statements")
 @GenerateWrapper
+@ReportPolymorphism
 public abstract class SLStatementNode extends Node implements
InstrumentableNode {

     private static final int NO_SOURCE = -1;

控制自動回報多型特化 #

排除特定節點與特化

ReportPolymorphism 註解套用於語言的所有節點是促進單態化的最簡單方法,但它可能會在不一定有意義的情況下導致回報多型特化。為了讓語言開發人員更精確地控制哪些節點和哪些特化會被納入考量以回報多型,引入了 @ReportPolymorphism.Exclude 註解,此註解適用於類別 (停用整個類別的自動回報) 或個別特化 (將這些特化排除在檢查多型時的考量範圍之外)。

僅回報巨型案例

從 20.3.0 版本開始,新增了一個新的註解:ReportPolymorphism.Megamorphic。此註解只能套用於特化,表示該特化為巨型,因為它旨在用於應該透過單態化修正的昂貴「通用」特化。新增此註解的效果是,一旦帶有註解的特化變成作用中,節點會向執行期回報多型,而不考慮其他特化的狀態。

此註解可以獨立於 @ReportPolymorphism 使用,也就是說,節點需要使用 @ReportPolymorphism 註解,巨型註解才能運作。如果同時使用兩個註解,則多型和巨型啟用都會回報為多型。

工具支援 #

語言開發人員應自行判斷哪些節點應該回報和不應該回報多型特化。這可以透過領域知識 (語言的哪些節點在多型時很昂貴) 或實驗 (測量包含/排除特定節點/特化的影響) 來完成。為了協助語言開發人員更好地了解回報多型特化的影響,我們提供了一些工具支援。

追蹤個別分割

在執行您的客體語言程式碼時,將 --engine.TraceSplitting 引數新增至命令列,將會即時印出執行期進行的每個分割的相關資訊。

以下是啟用旗標後執行其中一個 JavaScript 基準測試的一小部分輸出。

...
[engine] split   0-37d4349f-1     multiplyScalar |ASTSize      40/   40 |Calls/Thres       2/    3 |CallsAndLoop/Thres       2/ 1000 |Inval#              0 |SourceSection octane-raytrace.js~441-444:12764-12993
[engine] split   1-2ea41516-1     :anonymous |ASTSize       8/    8 |Calls/Thres       3/    3 |CallsAndLoop/Thres       3/ 1000 |Inval#              0 |SourceSection octane-raytrace.js~269:7395-7446
[engine] split   2-3a44431a-1     :anonymous |ASTSize      28/   28 |Calls/Thres       4/    5 |CallsAndLoop/Thres       4/ 1000 |Inval#              0 |SourceSection octane-raytrace.js~35-37:1163-1226
[engine] split   3-3c7f66c4-1     Function.prototype.apply |ASTSize      18/   18 |Calls/Thres       7/    8 |CallsAndLoop/Thres       7/ 1000 |Inval#              0 |SourceSection octane-raytrace.js~36:1182-1219
...

追蹤分割摘要

在執行您的客體語言程式碼時,將 --engine.TraceSplittingSummary 引數新增至命令列,將會在執行完成後印出關於收集到的分割資料的摘要。這包括分割的次數、分割預算的量和已使用的量、強制分割的次數、分割目標名稱的清單和分割的次數,以及回報多型特化的節點清單和次數。

以下是啟用旗標後執行其中一個 JavaScript 基準測試的稍加簡化的輸出。

[engine] Splitting Statistics
Split count                             :       9783
Split limit                             :      15342
Split count                             :          0
Split limit                             :        574
Splits                                  :        591
Forced splits                           :          0
Nodes created through splitting         :       9979
Nodes created without splitting         :      10700
Increase in nodes                       :     93.26%
Split nodes wasted                      :        390
Percent of split nodes wasted           :      3.91%
Targets wasted due to splitting         :         27
Total nodes executed                    :       7399

--- SPLIT TARGETS
initialize                              :         60
Function.prototype.apply                :        117
Array.prototype.push                    :          7
initialize                              :          2
magnitude                               :         17
:anonymous                              :        117
add                                     :          5
...

--- NODES
class ANode                             :         42
class AnotherNode                       :        198
class YetAnotherNode                    :          1
...

追蹤多型特化

請先閱讀分割指南,再閱讀本節,因為傾印的資料與分割的運作方式直接相關。

為了更好地了解回報多型如何影響哪些呼叫目標被考量進行分割,可以使用 --engine.SplittingTraceEvents 選項。此選項將即時印出一個記錄,詳細說明哪些節點正在回報多型,以及這如何影響呼叫目標。請參閱以下範例。

範例 1
[engine] [poly-event] Polymorphic event! Source: JSObjectWriteElementTypeCacheNode@e3c0e40   WorkerTask.run
[engine] [poly-event] Early return: false callCount: 1, numberOfKnownCallNodes: 1            WorkerTask.run

此記錄區段指出 WorkerTask.run 方法中的 JSObjectWriteElementTypeCacheNode 變成多型,並回報了它。它還指出這是第一次執行 WorkerTask.run (callCount: 1),因此您不會將其標記為「需要分割」(Early return: false)。

範例 2
[engine] [poly-event] Polymorphic event! Source: WritePropertyNode@50313382                  Packet.addTo
[engine] [poly-event] One caller! Analysing parent.                                          Packet.addTo
[engine] [poly-event]   One caller! Analysing parent.                                        HandlerTask.run
[engine] [poly-event]     One caller! Analysing parent.                                      TaskControlBlock.run
[engine] [poly-event]       Early return: false callCount: 1, numberOfKnownCallNodes: 1      Scheduler.schedule
[engine] [poly-event]     Return: false                                                      TaskControlBlock.run
[engine] [poly-event]   Return: false                                                        HandlerTask.run
[engine] [poly-event] Return: false                                                          Packet.addTo

在此範例中,多型特化的來源是 Packet.addTo 中的 WritePropertyNode。由於此呼叫目標只有一個已知的呼叫者,您可以分析其在呼叫樹中的父系 (即呼叫者)。在此範例中,它是 HandlerTask.run,同樣適用於它,進而適用於 TaskControlBlock.run,並依此類推至 Scheduler.scheduleScheduler.schedulecallCount 為 1,也就是說,這是第一次執行,因此您不會將其標記為「需要分割」(Early return: false)。

範例 3
[engine] [poly-event] Polymorphic event! Source: JSObjectWriteElementTypeCacheNode@3e44f2a5  Scheduler.addTask
[engine] [poly-event] Set needs split to true                                                Scheduler.addTask
[engine] [poly-event] Return: true                                                           Scheduler.addTask

在此範例中,多型特化的來源是 Scheduler.addTask 中的 JSObjectWriteElementTypeCacheNode。此呼叫目標會立即標記為「需要分割」,因為已符合所有條件。

範例 3
[engine] [poly-event] Polymorphic event! Source: WritePropertyNode@479cbee5                  TaskControlBlock.checkPriorityAdd
[engine] [poly-event] One caller! Analysing parent.                                          TaskControlBlock.checkPriorityAdd
[engine] [poly-event]   Set needs split to true                                              Scheduler.queue
[engine] [poly-event]   Return: true                                                         Scheduler.queue
[engine] [poly-event] Set needs split to true via parent                                     TaskControlBlock.checkPriorityAdd
[engine] [poly-event] Return: true                                                           TaskControlBlock.checkPriorityAdd

在此範例中,多型特化的來源是 TaskControlBlock.checkPriorityAdd 中的 WritePropertyNode。由於它只有一個呼叫者,因此您會查看該呼叫者 (Scheduler.queue),由於已符合所有必要的條件,因此您會將其標記為「需要分割」。

將多型特化傾印至 IGV

請先閱讀分割指南,再閱讀本節,因為傾印的資料與分割的運作方式直接相關。

在執行您的客體語言程式碼時,將 --engine.SplittingDumpDecisions 引數新增至命令列,每次呼叫目標被標記為「需要分割」時,都會傾印一個圖表,顯示節點鏈 (透過子系連線以及直接呼叫節點至被呼叫者根節點連線連結),最終連結到呼叫 Node#reportPolymorphicSpecialize 的節點。

與我們聯繫