ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

GraalVM Truffle 编译队列的遍历式调度机制:Traversing Compilation Queue 与动态编译阈值深度解析

GraalVM Truffle 编译队列的遍历式调度机制:Traversing Compilation Queue 与动态编译阈值深度解析 编译器JIT编译语言运行时高性能计算内存管理【免费下载链接】graalGraalVM compiles applications into native executables that start instantly, scale fast, and use fewer compute resources 项目地址https://gitcode.com/gh_mirrors/gr/graal点击查看免费下载导读本文围绕 truffle/docs/TraversingCompilationQueue.md 展开系统讲解 GraalVM Truffle 框架自 21.2.0 版本起引入的全新编译队列调度方案——Traversing Compilation Queue遍历式编译队列及其配套的 Dynamic Compilation Thresholds动态编译阈值机制。你将理解 Truffle 为何放弃朴素的 FIFO 编译队列、任务的优先级如何综合编译层级、编译历史与热度权重三要素动态判定以及四个--engine.DynamicCompilationThresholds*参数如何共同定义 scale 函数来动态缩放编译阈值文中所有结论均可在当前仓库源码TraversingBlockingQueue.java、CompilationTask.java、BackgroundCompileQueue.java 与 OptimizedRuntimeOptions.java中逐行验证。什么是编译队列Compilation Queue在解释执行interpretation阶段Truffle 会为每个 call target可调用的执行目标对应一段 guest 代码的根节点统计两个计数该 target 被执行的次数以及这些执行过程中发生的循环迭代次数二者合称call and loop count调用与循环计数。当这个计数达到某个阈值即编译阈值compilation threshold后该 call target 会被判定为热点hot并被调度去编译。为了让编译行为对 guest 代码的执行影响降到最低Truffle 并不会在解释器线程里同步编译而是把应当编译这个 target这一意图物化为一个编译任务任务载体CompilationTask.java负责记录任务优先级、目标引用、权重等状态任务队列BackgroundCompileQueue.java接收编译请求并负责调度执行者Truffle runtime 会派生若干个编译器线程数量由--engine.CompilerThreads控制它们从队列中取出任务并执行编译。关于编译器线程数量需要说明的是--engine.CompilerThreads的默认值为-1即不手动指定而是根据 CPU 核心数自动缩放参见 OptimizedRuntimeOptions.java 与 BackgroundCompileQueue.java 的缩放逻辑compilerThreads min(availableProcessors / 4 loglogCPU, 16)上限 16。手动设置时取值为[1, inf)栈大小可通过--engine.CompilerThreadStackSize指定默认 640KB。初代实现FIFO 队列的局限编译队列的初代实现是一个直白的 FIFO先进先出队列。这种方案在 guest 代码的预热warmup特性上存在明显缺陷并非所有 call target 都同样重要。目标是优先编译那些占用执行时间更多的 target从而更早获得更好的性能。但由于任务是在计数达到阈值的那一刻才入队的FIFO 队列实际上是按照达到阈值的先后顺序进行编译而这个顺序在实践中与真实的执行时间占比并不相关。考虑下面的 toy 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。遍历式编译队列Traversing Compilation Queue新的编译队列在 Truffle 中有一个形象的口语化名字——Traversing Compilation Queue它对以何种顺序编译 target采取了更动态的策略每当某个编译器线程请求下一个编译任务时队列都会遍历其中的所有条目挑选出优先级最高的那一个。从源码看TraversingBlockingQueue.java 实现了标准的BlockingQueueRunnable接口内部包装了一个双端阻塞队列实际是IdlingLinkedBlockingDeque。其核心是takeMax()方法L101-L132poll/take首先调用takeMax()尝试直接取出当前优先级最高的任务takeMax()被synchronized修饰保证同一时刻只有一个线程在遍历并挑选最大值遍历过程中CompilationTask.isCancelled()或updateWeight()返回 false目标已被 GC 回收的任务会被就地移除实现惰性清理由于使用的是弱一致性weakly consistent迭代器遍历期间队列仍可被其他线程写入因此解释器线程向队列添加任务不会被阻塞遍历完成后会把本次遍历耗时记录到任务的time字段、把队列变化量记录到queueChange字段供后续分析使用。任务的优先级如何判定一个任务的优先级由多个因素综合决定核心判定逻辑位于 CompilationTask.isHigherPriorityThan()。依次来看第一层编译层级tier优先。被调度进行第一层编译first-tier compilation的任务永远比第二层last/second tier任务拥有更高优先级。这样设计基于两点理由解释执行与第一层编译代码之间的性能差异远大于第一层与第二层编译代码之间的差异——因此尽早编译这些 target 收益更大第一层编译通常耗时更短一个编译器线程在完成一次第二层编译的相同时间内可以完成多个第一层编译。需要说明的是从当前源码看该机制的实现细节已经进一步演进默认开启多层级编译MultiTier默认true见 OptimizedRuntimeOptions.java第一层编译阈值默认 400、末层编译阈值默认 10000L202-L213。原文档也明确承认这种第一层永远优先的策略在某些场景下表现不佳未来版本可能继续改进。第二层编译历史。当两个任务处于同一编译层级时首先比较它们的编译历史给那些曾以更高编译层级编译过的任务更高优先级。例如某个 call target 完成了第一层编译随后因某种原因被失效invalidation又被重新排队进行第一层编译——那么它会优先于所有从未被编译过的第一层任务。理由很直接如果它之前被编译过说明它显然很重要不应因失效而受到超出必要程度的惩罚。第三层权重weight。如果前两个条件仍无法区分两个任务的优先级则优先选择权重更高的任务。值得一提的实现细节在 CompilationTask.java 的bonus()方法中还针对第一层任务默认系数TraversingQueueFirstTierBonus 15.0、被失效后重新编译的任务TraversingQueueInvalidatedBonus默认 1.0 即无加成、OSR栈上替换编译任务TraversingQueueOSRBonus默认 1.0分别施加可配置的优先级加成当TraversingQueueWeightingBothTiers默认true开启时两层任务都会按权重比较而不再只是第一层。权重weight的计算与缓存权重是 target 的 call and loop count 与时间的函数。原文档给出的定义是权重的值为 target 的 call and loop count 与其在过去 1ms 内增长速率rate的乘积。以 call and loop count 作为在该 target 上花费执行时间的代理这一指标旨在平衡该 target 上累计花费的总时间与该时间的近期增长速度两个方面从而给当前极热的 target 以优先级提升而不是给那些曾经热过但当前已很少执行的 target。当前仓库的实现CompilationTask.updateWeight()印证并细化了这一描述double currentRate rate(count, elapsed); ... lastRate lastRate * decay currentRate * (1 - decay); ... double weight (1 lastRate) * lastCount; lastWeight weight * bonus();其中速率并非简单取瞬时值而是引入了一个指数衰减decay 2^(-elapsed / halfLife)半衰期由--engine.TraversingQueueRateHalfLife配置默认 300ms见 OptimizedRuntimeOptions.java。这样计算出的速率既覆盖更长的观察窗口又对近期执行给予更高权重避免单个不可预测的采样区间扭曲热度判断。权重缓存出于性能考虑任务的权重会被缓存并复用 1ms源码中即1_000_000ns见 CompilationTask.java。若缓存值超过 1ms 才被重新计算权重在任务创建时以-1.0标记未初始化首次updateWeight时才真正计算。队列中的过期任务清理源码中还提供了一个原文档未展开、但对理解遍历行为很重要的细节由于权重基于近期活动速率长时间无活动的任务会被判定为stale过期并从队列中移除isStale()CompilationTask.java。任务在TraversingQueueStaleTaskDelay默认 100ms置 0 可关闭内无调用活动即视为过期从而避免编译器线程把时间浪费在已经冷却的 target 上。开关与默认状态遍历式编译队列自 21.2.0 起默认开启可通过以下选项关闭--engine.TraversingCompilationQueuefalse对应源码OptimizedRuntimeOptions.java默认值true。在 BackgroundCompileQueue.java 中可以看到开启时使用TraversingBlockingQueue关闭时回退到原来的IdlingPriorityBlockingQueue即基于优先级与 FIFO 的旧行为。动态编译阈值Dynamic Compilation Thresholds动机队列规模必须可控遍历式编译队列的一个固有问题是为拿到最新权重并选出最高优先级任务它需要遍历队列中的所有条目。只要队列规模保持在合理范围内这一遍历的开销并不显著。但反过来为了在合理时间内总是选出最高优先级任务就必须保证队列不会无限增长。这由一种被称作dynamic compilation thresholds动态编译阈值的机制实现其实现位于 BackgroundCompileQueue.java 内部的DynamicCompilationThresholds类。简单说动态编译阈值意味着编译阈值即每个 call target 的 call and loop count 所对照的那个阈值会随队列状态随时间变化如果队列过载overloaded就提高编译阈值减少新编译任务的流入——即 target 必须更热才有资格被调度编译反之如果队列接近空闲就降低编译阈值允许更多 target 被调度——即编译器线程面临空闲风险那就把没那么热的 target 也交给它们。load 与 scale 函数阈值的变化被称为scaling缩放因为实际阈值就是基础阈值乘以一个由scale函数决定的缩放因子。scale函数的输入是队列的load负载定义为load 队列中的任务数 / 编译器线程数之所以刻意除以编译器线程数是因为队列中的原始任务数并不能很好地反映编译压力。文档给出了一个直观的例子假设平均一次编译耗时 100ms队列中有 160 个任务——16 线程的运行时大约需要10 * 100ms即约 1 秒完成全部任务而只有 2 个编译器线程的运行时则需要大约80 * 100ms即约 8 秒。同样的任务数压力截然不同。这一公式在源码中对应load()方法BackgroundCompileQueue.java。scale函数由 4 个参数定义全部可在源码 OptimizedRuntimeOptions.java 中验证参数默认值含义--engine.DynamicCompilationThresholdsMinScale0.1允许将阈值缩放到的下限即阈值永远不会被缩放到默认值的 10% 以下--engine.DynamicCompilationThresholdsMinNormalLoad0不缩放阈值所需的最小负载只要队列负载高于该值运行时就不会向下缩放阈值--engine.DynamicCompilationThresholdsMaxNormalLoad90不缩放阈值所需的最大负载只要队列负载低于该值运行时就不会向上缩放阈值--engine.DynamicCompilationThresholdsHighLoadSlope0.09队列负载超过 MaxNormalLoad 后scale 函数的线性斜率逐参数解读DynamicCompilationThresholdsMinScale默认 0.1定义我们愿意把阈值缩放到多低。默认值 0.1 意味着编译阈值永远不会被缩放到其默认值的 10% 以下。当DynamicCompilationThresholdsMinNormalLoad大于 0 时按定义有scale(0) DynamicCompilationThresholdsMinScale当DynamicCompilationThresholdsMinNormalLoad为 0 时低负载阈值下调被禁用——这正是默认行为。DynamicCompilationThresholdsMinNormalLoad默认 0定义阈值不会被缩放的最小负载。只要队列负载高于该值运行时就不会下调编译阈值。按定义有scale(DynamicCompilationThresholdsMinNormalLoad) 1即默认值下scale(0) 1。DynamicCompilationThresholdsMaxNormalLoad默认 90定义阈值不会被缩放的最大负载。只要队列负载低于该值运行时就不会上调编译阈值。按定义有scale(DynamicCompilationThresholdsMaxNormalLoad) 1即默认值下scale(90) 1。DynamicCompilationThresholdsHighLoadSlope默认 0.09高负载区间的线性斜率。源码scale()方法L582-L591显示高负载分支的公式为highLoadSlope * (x - maxNormalLoad) 1即一条穿过点(MaxNormalLoad, 1)、斜率为HighLoadSlope的直线。scale 函数的完整定义到目前为止我们在 3 个点上定义了scale函数。所有介于这些点之间的取值scale都是连接两点的直线介于最小与最大正常负载之间的所有值scale按定义恒为 1介于 0 与最小正常负载之间的值scale在最小缩放值与 1 之间线性增长若最小正常负载为 0则 scale 函数不会为低负载降低阈值在函数定义域的剩余部分即大于最大正常负载的值scale是斜率为DynamicCompilationThresholdsHighLoadSlope、穿过点(DynamicCompilationThresholdsMaxNormalLoad, 1)的线性函数。这一点在源码的构造函数中体现得尤为清晰BackgroundCompileQueue.javathis.lowLoadSlope minNormalLoad 0 ? 0 : (1 - minScale) / minNormalLoad; this.highLoadSlope highLoadSlope;即低负载分支的斜率lowLoadSlope由(1 - minScale) / minNormalLoad推导得出保证在minNormalLoad处恰好达到 1若minNormalLoad 0则斜率为 0低负载下调被禁用。下图是文档给出的 scale 函数 ASCII 示意图直观展示了它的分段线性形状^ scale | | / | / | / | / | / | / 1 |..... ________________________________/ | /. . | / . . | / . . | / . . | / . . MinScale |/ . . | . . |_______________________________________________________ load 0 ^ ^ MinNormalLoad MaxNormalLoad缩放如何生效在 BackgroundCompileQueue.java 中scaleThresholds()会在任务提交submit和编译前后beforeExecute/afterExecute被调用把计算出的缩放因子通过runtime.setCompilationThresholdScale(FixedPointMath.toFixedPoint(scale()))写入运行时随后每个 call target 对照的编译阈值即为基础阈值第一层默认 400、末层默认 10000乘以该缩放因子。依赖关系与开关需要特别注意两点约束动态编译阈值只与遍历式编译队列配合工作二者自 21.2.0 起均默认开启从源码 BackgroundCompileQueue.java 可见动态阈值还需满足DynamicCompilationThresholds与BackgroundCompilation同时为真才会被实例化。可以通过如下选项单独关闭动态编译阈值--engine.DynamicCompilationThresholdsfalse对应源码默认值trueOptimizedRuntimeOptions.java。组合使用与调优建议遍历式编译队列与动态编译阈值是配套设计的两层机制可以在运行 Truffle 语言如 GraalVM 的 JavaScript、Python、Ruby 等时通过引擎选项--engine.*或编程方式下的 Engine 配置进行调优调优目标推荐参数组合观察编译调度过程--engine.TraceCompilationtrue、--engine.TraceCompilationDetailstrue回退到旧 FIFO 行为--engine.TraversingCompilationQueuefalse会同时失去动态阈值效果关闭阈值动态缩放--engine.DynamicCompilationThresholdsfalse放宽/收紧队列压力控制调整DynamicCompilationThresholdsMaxNormalLoad与DynamicCompilationThresholdsHighLoadSlope调整编译器线程数--engine.CompilerThreadsn默认-1自动缩放需要强调的是这些参数大多属于内部INTERNAL或专家EXPERT级别的选项其默认值经过 GraalVM 团队的工程实践校准如MaxNormalLoad90、HighLoadSlope0.09、MinScale0.1普通场景不建议随意改动只有在通过TraceCompilation系列日志确认队列持续过载或编译器线程大量空闲时才值得针对性调整。总结Truffle 的编译队列从先到先编译的 FIFO 演进为每次遍历、动态取优的遍历式队列本质上是把编译资源的分配从入队顺序驱动改为执行热度驱动先编译执行时间占比高、最近仍在快速增长热度的 target并通过对失效任务的优先级补偿避免热点反复惩罚。而动态编译阈值则从供给侧守护这一机制——通过一个分段线性的 scale 函数依据任务数/编译器线程数定义的负载动态缩放编译阈值在队列过载时提高门槛、在队列空闲时降低门槛从而既保证队列遍历的成本可控又尽量不让任何编译器线程空转。两者合力构成了自 21.2.0 起 Truffle 默认的编译调度方案其完整实现散落在 TraversingBlockingQueue.java、CompilationTask.java 与 BackgroundCompileQueue.java 中所有选项的默认值与语义均可在 OptimizedRuntimeOptions.java 中逐一核对是研究 Truffle 预热与自适应编译行为的良好起点。赞分享编译器JIT编译语言运行时高性能计算内存管理【免费下载链接】graalGraalVM compiles applications into native executables that start instantly, scale fast, and use fewer compute resources 项目地址https://gitcode.com/gh_mirrors/gr/graal点击查看免费下载相关推荐GraalVM Truffle 宿主编译Host Compilation实战指南解释器代码的内联优化与调优GraalVM Truffle 宿主编译Host Compilation实战指南解释器代码的内联优化与调优 导读 本文聚焦 GraalVM Truffle编译器JIT编译语言运行时高性能计算内存管理GraalVM 编译器 Replay Compilation 实战指南录制与复现编译任务GraalVM 编译器 Replay Compilation 实战指南录制与复现编译任务 GraalVM 编译器Graal Compiler提供了一套名为编译器JIT编译语言运行时高性能计算内存管理p-queue深度解析优先级队列与任务调度机制p queue深度解析优先级队列与任务调度机制 在现代JavaScript开发中高效管理异步任务执行是提升应用性能的关键。 p queue 作为一款功能强大上一篇Bicep Registry Modules企业级部署大规模基础设施管理经验下一篇终极IDM激活指南如何永久延长试用期的完整教程创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表