Loader V2:消除 Move VM 中最大的性能瓶颈

  • aptoslabs
  • 发布于 2025-03-22 15:53
  • 阅读 13

文章介绍了 Aptos 为 Move VM 重构的 Loader V2。

George Mitenkov

TL;DR:Loader V2 是 Move VM 代码加载与缓存基础设施的一次重大升级。

  • Loader V2 使用多级、线程共享的代码缓存,显著减少代码加载时间。实际测试中,基于 Aptos mainnet 上的实验,区块执行速度最高可提升 60%。
  • Loader V2 集成到了 Aptos 并行执行引擎(Block-STM)中,使智能合约的并行升级成为可能。根据我们的基准测试,包含 Move 代码升级交易的区块速度约快 10 倍。
  • 这一新设计还使 Move VM 变得无状态、线程安全且更可靠。

背景

Aptos 区块链通过 Block-STM 执行引擎来执行一个区块中的交易。交易会以乐观并行的方式执行,就像它们之间不存在数据依赖一样。Block-STM 会管理每笔交易的读取和写入,以检测冲突,并在必要时重新执行发生冲突的交易。Block-STM 还使用 rolling commit——一种动态检测交易何时可以被提交的机制,也就是说,此时它将不再被重新执行。

每个 Block-STM 线程都会运行一个 Move virtual machine(VM)实例,用于解释交易指定的 Move bytecode。在执行过程中,Move VM 会通过一个名为 loader 的组件获取存储智能合约代码的 modules。loader 会对 modules 进行反序列化,验证它们及其传递依赖项(例如使用了其他合约时),并将验证后的 modules 缓存在 Move VM 的 cache 中。Modules 也可以被 republished(即智能合约被升级),而 loader 会确保新代码能够被正确使用和链接。

图 1:传统 loader 架构,以及它如何融入 Aptos 区块链使用的 Block-STM 并行执行引擎。每个线程都运行一个带有各自 module cache 的 VM 实例,这些 cache 会随着不同交易的执行而被填充。

问题

传统 loader 在 module loading 和升级方面的语义非常依赖具体实现,细节复杂且脆弱,有时会在 Aptos 区块链用户尝试发布 package 时导致意外错误。除此之外,传统 loader 还是一个性能瓶颈。

Move VM 拥有 module cache。 这意味着 module cache 是按线程划分的,并且不会在 Block-STM 运行的不同 VM 实例之间共享。因此,并行加载(即从 storage 获取、反序列化、验证)同一个带有大量依赖的 module 的计算代价非常高。比如,如果 Block-STM 使用 32 个线程,而每个线程都尝试加载同一个 module 及其依赖,那么在最坏情况下,每个 module 会被加载 32 次。

Module cache 只会被一个区块中的交易使用,然后被丢弃。 这是因为 module cache 的生命周期受限于 Block-STM 使用的 VM 实例的生命周期。因此,如果跨区块的交易使用同一个智能合约,它会在每个区块中都被加载一次。对于频繁访问的热点合约来说,这尤其糟糕,例如 Aptos framework。

图 2:并行推测执行交易时,可能会导致非确定性行为。Block-STM 会检测这些情况并顺序执行交易。

如果智能合约代码被升级,Block-STM 可能会顺序执行整个交易区块。 由于 Aptos 区块链上的 Move 智能合约是可升级的,因此区块中的某些交易可能会发布 module 的新版本,例如添加新函数或修改现有实现。不幸的是,传统 loader 架构根本无法与 Block-STM 使用的推测并行执行兼容。这是因为某个线程(T2)可能会读取另一个线程(T1)推测性发布的代码,将其加载并缓存到 T2 的 cache 中(图 2)。但如果 Block-STM 检测到 T1 的执行结果本应不同,从而导致该 module 不应被发布,并重新执行 T1,那么 T2 的 loader cache 就不再一致:它存储了本不应被发布的代码版本。这种行为会导致用户意料之外的错误,在最坏情况下,还会在验证者之间产生非确定性的执行结果。为缓解这个问题,Block-STM 会在智能合约被升级且同时被访问的区块上回退到顺序执行。因此,在这种情况下,性能会下降一个数量级。

解决方案

Loader V2 是对传统 loader 的一次彻底重设计。新实现中最重要的一点是,loader 及其 cache 不再属于 Move VM,这使得线程之间可以共享 cache。实际上,这也意味着,为了在 VM 中执行一个 Move 智能合约,必须同时向它提供一个 loader,如图 3 所示。

图 3:Loader V2 架构,以及它如何融入 Move VM 和 Block-STM 执行。最初,L3 包含 module 0x1::x 。在区块 1 中:(1) 第一个交易从 L3 cache 读取 0x1::x ,并将其记录到本地 L1 cache;(2) 第一个交易再次读取 0x1::x ,这次来自 L1 cache;(3) 第二个交易读取 0x1::y ,它在 L3 cache 和 L2 cache 中都未命中;(4) 该 module 从 storage 中取出并缓存到 L2 cache;(5) 第三个交易从 L2 cache 读取 module 0x1::y (此时 L3 cache 中仍然未命中)。当区块 1 执行完成后,L2 中的缓存条目会被移动到 L3: (6) module 0x1::y 被移动到 L3 cache。然后执行第二个区块。在区块 2 中:(7) 第一个交易从 L3 cache 读取 0x1::y ,并将读取到的 module 复制到本地 L1 cache;(8) 第二个交易写入 0x1::y 的新版本,使 L3 cache 中的条目失效,并将新条目加入 L2 cache。之后对 0x1::y 的访问将会从 L2 cache 中解析到 0x1::y

Loader V2 使用的 module cache 可以跨多个区块使用,并且分为 3 个层级。

  1. L3: 一个全局无锁 module cache,生命周期最长可达一个 epoch(按本文撰写时,在 Aptos mainnet 上最长可达 2 小时)。仅在 epoch 边界、节点配置变更时,或内存使用超过某个限制时才会清空。
  2. L2: 一个用于单个交易区块的并发 module cache。当交易访问在 L3 cache 中不存在的 modules 时,这些 modules(以及它们的依赖,如果已加载)会被放入 L2 cache。区块执行结束时,L2 cache 中的条目会被提升到无锁的 L3 cache。因此,未来对这些 modules 的访问将受益于零同步开销。
  3. L1: 一个用于单笔交易执行的线程本地 module cache。当交易(Move VM)从 L2 或 L3 cache 读取一个 module 时,会把其副本(准确地说,是一个指针)存入 L1 cache。这样,对同一个 module 的后续读取就是无锁的,并且会解析到同一个 module。

接下来,让我们看看这些分层 cache 如何与 module publishing 协同工作。

记住我以便更快登录

当交易写入数据时,Block-STM 会在一个多版本数据结构中记录其写入。写入可能会使其他交易的推测执行失效,在这种情况下,这些交易会被安排重新执行,从而产生新的写入版本。

在新设计中,对于发布代码的交易,module 写入在执行结束时不会对其他交易可见。相反,Loader V2 依赖 Block-STM 的 rolling commit 机制,使 module 写入只有在交易被提交时才可见(即可以保证它不再被重新执行)。这种方法的好处是,module 信息不需要版本化:module cache 只需要存储 module 的最新版本。

当 module 写入变为可见时,会发生几件事:

  1. L3 cache 中对应的条目会被标记为无效。
  2. 已发布的 module 会被放入 L2 cache。
  3. 已提交交易之后的交易会重新验证,如果它们读取了旧的 module 版本,则会重新执行。因此,受影响的交易最终会读取更新后的代码版本,并使用升级后的版本执行交易。

请注意,步骤 (1) — (2) 是为了优化常见场景:读密集型 workload。直接将新 module 添加到 L3 cache 需要锁和同步原语,从而在每次访问时引入额外开销。

结果

Loader V2 已在 AIP-107 中描述,并且该变更已包含在 Aptos node binary 的 1.26 版本中。Loader V2 最近已在 Aptos mainnet 上启用。

向后兼容性

Loader V2 与传统 loader 实现几乎完全向后兼容。然而,在 Aptos mainnet 和 testnet 网络的交易历史中,确实存在少数执行行为出现分歧的情况。我们分析了所有分歧案例,并得出结论:这些历史行为并非预期结果,而是传统 loader 实现不佳导致的。例如,我们发现有些历史交易失败,是因为传统 loader 错误地使用了旧版本代码。使用 Loader V2 时,这些交易会使用正确版本的代码,从而产生不同的执行输出。显然,在这种情况下维持与传统实现的兼容性是不现实的(严格来说,甚至根本不可能)。

性能

最初,Loader V2 是在一组人工基准测试上评估的,这些测试衡量的是单节点在执行不同 workload 时的吞吐量,例如转账、module publish 或 NFT mint。我们观察到,对于会发布 module 的 workload,吞吐量大约可提升 ~10 倍。

然而,现有基准测试套件并没有像真实网络那样包含多样化的不同 modules,因此对于常见场景——读密集型交易来说,代表性还不够。于是,我们在 Aptos mainnet 上进行了实验。

由于 loader V2 几乎完全向后兼容,因此可以将其启用在 Aptos Labs 运行的某个 validator 节点上。启用后,我们比较了使用 Loader V2 的节点与其他节点之间的平均区块执行时间,如图 4 所示。

图 4:8 个不同 validator 节点的平均区块执行时间(单位:秒)。mainnet-validator-usce1–0-aptos-node-validator-0 节点启用了 Loader V2(底部曲线)。其他节点使用传统 loader。

使用 Loader V2 后,平均区块执行时间显著降低,有时最高可减少 60%。这种加速之所以能够实现,是因为新 loader 实现使用了长生命周期的(L3)cache。

结论

Loader V2 是对 Move VM 传统 loader 的一次完整重设计,负责代码加载和缓存。新实现已从 Move VM 中移出,使其变得无状态且线程安全。Loader V2 已集成到 Block-STM 中,使其能够并行执行包含 module publish 的交易区块。同时,还引入了用于缓存已加载并验证过的 Move 智能合约的长生命周期 cache,从而在多样化的真实 workload 上大幅缩短了区块执行时间。

  • 原文链接: medium.com/aptoslabs/loa...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
aptoslabs
aptoslabs
江湖只有他的大名,没有他的介绍。