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 * 100ms(即 1 秒)内完成所有任务。另一方面,具有 2 个编译器线程的运行时将花费大约 80 * 100ms(即 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 函数是连接这两个点的直线。这意味着,对于最小和最大正常负载之间的所有值,缩放函数根据定义为 1。对于 0 到最小正常负载之间的值,scale 函数在最小缩放和 1 之间线性增长。让我们将此函数的斜率定义为 s。现在,对于函数域的其余部分,即大于最大正常负载的值,我们将 scale 定义为通过点 (DynamicCompilationThresholdsMaxNormalLoad, 1) 的斜率为 s 的线性函数。

以下是 scale 函数的 ASCII 艺术图,它应该说明了它的定义方式。

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

动态阈值仅适用于遍历编译队列,并且从 21.2.0 版本开始默认处于启用状态。可以使用 --engine.DynamicCompilationThresholds=false 禁用它们。

联系我们