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();
}

lowUsage函数和highUsage函数即使在首次执行时,也将达到足够高的调用和循环计数阈值,但lowUsage函数会首先达到。使用FIFO队列,我们将首先编译lowUsage函数,尽管此示例表明,为了更快地达到更好的性能,应优先编译highUsage函数。

遍历编译队列 #

Truffle中的新编译队列,口语上称为“遍历编译队列”,采用了一种更动态的方法来选择编译目标的顺序。每当编译器线程请求下一个编译任务时,队列将遍历队列中的所有条目,并选择优先级最高的那个。

任务的优先级根据几个因素确定

首先,调度进行一级编译(即一级任务)的目标总是比二级任务具有更高的优先级。这背后的原因是,在解释器中执行代码与在一级编译代码中执行代码之间的性能差异远大于一级和二级编译代码之间的差异,这意味着我们能更快地从编译这些目标中获得更多收益。此外,一级编译通常耗时较少,因此一个编译器线程可以在完成一个二级编译所需的时间内完成多个一级编译。这种方法在某些场景下表现不佳,并可能在未来版本中得到改进。

当比较同一层级的两个任务时,我们首先考虑它们的编译历史,并优先处理之前曾被更高编译器层级编译过的任务。例如,如果一个调用目标被一级编译,然后因某种原因失效,接着再次被排队进行一级编译,它将优先于所有其他从未被编译过的一级目标。理由是,如果它之前被编译过,则显然很重要,不应因其失效而受到不必要的额外惩罚。

最后,如果前两个条件无法区分两个任务的优先级,我们会优先处理“权重”更高的任务。权重是目标调用和循环计数以及时间的函数。它定义为目标的调用和循环计数与该调用和循环计数在过去1毫秒内增长速率的乘积。使用目标的调用和循环计数作为执行该调用目标所花费时间的代理,这个指标旨在平衡执行该调用目标所花费的总时间与该时间的近期增长。这在与那些“热点”但目前不常执行的目标进行比较时,会给那些当前“非常热点”的目标带来优先级提升。

出于性能考虑,任务的权重会被缓存并在1毫秒的周期内重用。如果缓存值超过1毫秒,则会重新计算。

遍历编译队列自21.2.0版本起默认启用,并可通过使用--engine.TraversingCompilationQueue=false来禁用。

动态编译阈值 #

遍历编译队列的一个问题是,它需要遍历队列中的所有条目以获取最新的权重并选择优先级最高的任务。只要队列的大小保持合理,这不会对性能产生显著影响。这意味着为了始终在合理的时间内选择优先级最高的任务,我们需要确保队列不会无限增长。

这通过我们称之为“动态编译阈值”的方法实现。简单来说,动态编译阈值意味着编译阈值(即在决定是否编译每个调用目标时,调用和循环计数所比较的阈值)可能会根据队列的状态随时间变化。如果队列过载,我们旨在提高编译阈值以减少传入的编译任务数量,即,目标需要“更热”才能被调度进行编译。另一方面,如果队列接近空,我们可以降低编译阈值以允许更多目标被调度进行编译,即,编译线程有空闲的危险,所以让我们给它们编译“不那么热”的目标。

我们将这种阈值的改变称为“缩放”,因为在实践中,阈值只是通过由scale函数确定的“缩放因子”进行乘法运算。scale函数将队列的“负载”作为输入,队列负载是队列中的任务数量除以编译器线程数量所得的值。我们有意地根据编译器线程的数量进行控制,因为队列中任务的原始数量并不能很好地代表编译压力有多大。例如,假设平均每次编译需要100毫秒,并且队列中有160个任务。一个拥有16个线程的运行时将在大约10 * 100ms即1秒内完成所有任务。另一方面,一个拥有2个编译器线程的运行时将需要大约80 * 100ms即8秒。

scale函数由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禁用。

联系我们