Truffle 編譯佇列方法

從 21.2.0 版本開始,Truffle 採用新的編譯佇列方法。此文件提供了此方法的動機和概述。

什麼是編譯佇列? #

在執行客體程式碼期間,每個 Truffle 呼叫目標都會計算其執行次數以及這些執行期間發生的迴圈迭代次數 (即目標的「呼叫和迴圈計數」)。一旦此計數器達到特定閾值,該呼叫目標就會被視為「熱點」並排程進行編譯。為了最大程度地減少此行為對客體程式碼執行的影響,應編譯目標的概念將具體化為 編譯任務 並放入 編譯佇列 中等待編譯。Truffle 執行階段會產生數個編譯器執行緒 (--engine.CompilerThreads),這些執行緒會從佇列中取得任務並編譯指定的呼叫目標。

Truffle 中編譯佇列的初始實作是一個簡單的 FIFO 佇列。這種方法在客體程式碼執行的預熱特性方面有重要的限制。也就是說,並非所有呼叫目標都同樣重要需要編譯。目的是找出佔用更多執行時間的目標並首先編譯它們,從而更快地達到更好的效能。由於呼叫目標會在計數器達到特定閾值時排入佇列進行編譯,因此 FIFO 佇列會按達到該閾值的順序編譯目標,這在實務上與實際執行時間無關。

請考慮以下玩具 JavaScript 範例

function lowUsage() {
    for (i = 0; i < COMPILATION_THRESHOLD; i++) {
        // Do something
    }
}

function highUsage() {
    for (i = 0; i < 100 * COMPILATION_THRESHOLD; i++) {
        // Do something
    }
}

while(true) {
    lowUsage();
    highUsage();
}

即使在第一次執行時,lowUsagehighUsage 函式都會達到足夠高的呼叫和迴圈計數閾值,但 lowUsage 函式會先達到。使用 FIFO 佇列,我們會先編譯 lowUsage 函式,即使此範例說明應先編譯 highUsage 函式以更快地達到更好的效能。

遍歷編譯佇列 #

Truffle 中的新編譯佇列,俗稱 「遍歷編譯佇列」,採用更動態的方法來選擇編譯目標的順序。每次編譯器執行緒要求下一個編譯任務時,佇列都會遍歷佇列中的所有項目,並選取優先順序最高的項目。

任務的優先順序 取決於幾個因素

首先,排程用於 第一層編譯 的目標 (即第一層任務) 始終比第二層任務具有更高的優先順序。這背後的理由是,在直譯器中執行程式碼與在第一層編譯程式碼中執行程式碼之間的效能差異,遠大於第一層和第二層編譯程式碼之間的差異,這表示我們從盡快編譯這些目標中獲得更多好處。此外,第一層編譯通常花費較少時間,因此一個編譯器執行緒可以在完成一個第二層編譯的相同時間內完成多個第一層編譯。這種方法在某些情況下表現不佳,並且可能會在未來的版本中得到改進。

當比較兩個相同層級的任務時,我們會先考量其編譯歷史記錄,並優先處理先前使用較高編譯器層級編譯的任務。例如,如果呼叫目標進行第一層編譯,然後因某些原因而失效,然後再次排入第一層編譯的佇列,則它會優先於先前從未編譯過的所有其他第一層目標。其原因是,如果先前已編譯,則它顯然很重要,並且不應因其失效而受到不必要的懲罰。

最後,如果前兩個條件無法區分兩個任務之間的優先順序,則我們會優先處理具有較高「權重」的任務。權重是目標的呼叫和迴圈計數以及時間的函數。它定義為目標的呼叫和迴圈計數與該呼叫和迴圈計數在過去 1 毫秒內的成長率的乘積。將目標的呼叫和迴圈計數用作執行該呼叫目標所花費的時間量的代理,此指標旨在平衡執行該呼叫目標所花費的總時間與該時間最近的成長。與先前「熱點」但目前未被大量執行的目標相比,這會優先提升目前「非常熱點」的目標。

為了效能起見,任務的權重會快取並重複使用 1 毫秒的時間。如果快取值已超過 1 毫秒,則會重新計算。

遍歷編譯佇列在 21.2.0 版本中預設為開啟,可以使用 --engine.TraversingCompilationQueue=false 停用。

動態編譯閾值 #

遍歷編譯佇列的一個問題是,它需要遍歷佇列中的所有項目才能取得最新的權重,並選擇優先順序最高的任務。只要佇列的大小保持合理,這就不會對效能產生重大影響。這表示為了始終在合理的時間內選擇優先順序最高的任務,我們需要確保佇列不會無限期地增長。

這是透過我們稱為 「動態編譯閾值」 的方法來實現的。簡單來說,動態編譯閾值表示編譯閾值 (每個呼叫目標的呼叫和迴圈計數與之比較,以確定是否編譯它) 可能會隨著時間的推移而改變,具體取決於佇列的狀態。如果佇列過載,我們的目標是提高編譯閾值,以減少傳入的編譯任務數量,也就是說,目標需要「更熱」才能排程進行編譯。另一方面,如果佇列接近空白,我們可以降低編譯閾值,以允許排程更多目標進行編譯,也就是說,編譯執行緒有閒置的風險,因此讓我們提供它們甚至「較不熱」的目標進行編譯。

我們將閾值的這種變化稱為「縮放」,因為實際上閾值只是乘以由 scale 函式決定的「縮放係數」。縮放函式將佇列的「負載」作為輸入,該負載是佇列中的任務數除以編譯器執行緒數。我們刻意控制編譯器執行緒的數量,因為佇列中的原始任務數並不是編譯壓力的好代理。例如,假設平均編譯需要 100 毫秒,並且佇列中有 160 個任務。具有 16 個執行緒的執行階段將在大約 10 * 100 毫秒 (即 1 秒) 內完成所有任務。另一方面,具有 2 個編譯器執行緒的執行階段將大約需要 80 * 100 毫秒,即 8 秒。

縮放函式由 3 個參數定義:--engine.DynamicCompilationThresholdsMinScale--engine.DynamicCompilationThresholdsMinNormalLoadDynamicCompilationThresholdsMaxNormalLoad

--engine.DynamicCompilationThresholdsMinScale 選項定義了我們願意將閾值縮放到的最低程度。它的預設值為 0.1,表示編譯閾值永遠不會縮放到其預設值的 10% 以下。這在實務上表示,根據定義,scale(0) = DynamicCompilationThresholdsMinScale 或對於預設值 scale(0) = 0.1

--engine.DynamicCompilationThresholdsMinNormalLoad 選項定義了編譯閾值將不會縮放的最小負載。這表示只要佇列的負載高於此值,執行階段就不會向下縮放編譯閾值。這在實務上表示,根據定義,scale(DynamicCompilationThresholdsMinScale) = 1 或對於預設值 scale(10) = 1

--engine.DynamicCompilationThresholdsMaxNormalLoad 選項定義了編譯閾值將不會縮放的最大負載。這表示只要佇列的負載低於此值,執行階段就不會向上縮放編譯閾值。這在實務上表示,根據定義,scale(DynamicCompilationThresholdsMaxScale) = 1 或對於預設值 scale(90) = 1

到目前為止,我們已經在 3 個點定義了 scale 函數。對於這幾個點之間的所有值,scale 函數都是連接這兩個點的直線。這表示對於最小正常負載和最大正常負載之間的所有值,根據定義,scale 函數為 1。對於 0 和最小正常負載之間的值,scale 函數在最小縮放比例和 1 之間線性增長。讓我們將此函數的斜率定義為 s。現在,對於函數域的其餘部分,即大於最大正常負載的值,我們將 scale 定義為斜率為 s 的線性函數,且該函數通過點 (DynamicCompilationThresholdsMaxNormalLoad, 1)

以下是 scale 函數的 ASCII 圖,應可說明其定義方式。

          ^ scale
          |
          |                                            /
          |                                           /
          |                                          /
          |                                         /
          |                                        /
          |                                       /
        1 |..... ________________________________/
          |     /.                               .
          |    / .                               .
          |   /  .                               .
          |  /   .                               .
          | /    .                               .
MinScale >|/     .                               .
          |      .                               .
          |_______________________________________________________> load
         0       ^                               ^
              MinNormalLoad                   MaxNormalLoad

動態閾值僅適用於遍歷編譯佇列,並且從 21.2.0 版本開始預設為啟用。可以使用 --engine.DynamicCompilationThresholds=false 停用它們。

與我們聯繫