Lazy Loading:Composable Move 的新篇章

  • aptoslabs
  • 发布于 2026-01-15 23:32
  • 阅读 9

本文介绍了 Aptos Move VM 从 Eager Loading 转向 Lazy Loading 的原因与效果。

TL;DR:

  • Eager Loading(旧方案)要求在部署或执行 Move smart contract 之前,对其所有传递依赖进行 gas metering。这对于现实中的 DeFi protocols 来说过于严格。
  • Lazy Loading(新方案)会在执行期间首次使用模块时对其进行 metering 和加载;在部署期间,它只对 package 以及 package 的直接依赖进行 metering。Lazy Loading 降低了触发 dependency-limit 失败的可能性,减少了常见情况下的 gas 消耗,并提供了同等水平的性能。

Move VM 中的 Module Loading

Move Virtual Machine (VM) 使用一个名为 loader 的组件来获取包含 Move bytecode 的 modules。Modules 可以依赖其他 modules,从而支持 smart contract 的可组合性。开发者可以通过在链上发布新的 bytecode 来升级 modules。

当用户执行 smart contract 中的某个函数时,VM 会加载定义该函数的 module,然后解释执行 bytecode。加载一个 module 不只是“读取字节”:loader 会从存储中获取 module code,进行反序列化,并验证它。

一直以来,VM 采用的是 Eager Loading 方法:通过加载某个模块及其依赖、依赖的依赖等,来验证该 module 是否能够正确链接到其声明的 dependencies。最坏情况下,可能会加载一个 module 的所有传递依赖。

为了限制模块加载所耗费的时间并防止拒绝服务攻击,VM 会对每次 module load 收取 gas,并限制任何 module 可拥有的传递依赖数量。使用 eager loading 时,最坏情况是加载一个 module 的所有传递依赖,因此 VM 无法在“事后”安全地对它们进行 metering。相反,VM 必须预先遍历所有传递依赖,以便:

  • 按可能需要加载的 modules 数量收取 gas
  • 强制执行传递依赖最大数量的硬性限制。

不幸的是,这种设计损害了 smart contract 的可组合性,并限制了流行的 DeFi use cases。

DEX Aggregator 示例

链上的 DEX aggregators 是常见的 DeFi contracts,允许用户在同一条区块链内交换资产。下面是一个用 Move 编写的此类 aggregator module 的最小示例:它允许用户在某个 exchange 上将类型 X 的资产兑换为类型 Y 的资产。

module 0x99::dex_aggregator {
  // 该模块的所有 dependencies。
  use 0x1::fungible_asset;
  use 0x123::dex_a;
  use 0x456::dex_b;

  // 支持的 DEX 列表。
  const DEX_A: u8 = 0;
  const DEX_B: u8 = 1;

  // 在选定的 DEX 上将某些资产 X 兑换为资产 Y。
  public entry fun swap<X, Y>(user: &signer, amount: u64, dex: u8) {
    let asset_x = fungible_asset::withdraw<X>(user, amount);

    let asset_y = if (dex == DEX_A) {
       dex_a::swap<X, Y>(asset_x)
    } else if (dex == DEX_B) {
       dex_b::swap<X, Y>(asset_x)
    } else {
       // 这个 DEX 未知 - 中止。
      abort 1;
   };

    fungible_asset::deposit<Y>(user, asset_y);
  }
}

图 1:一个用 Move 编写的 DEX aggregator module 示例。这个 aggregator module 有 3 个 dependencies:0x123::dex_a 0x456::dex_b 0x1::fungible_asset 。这些 modules 都各自还有自己的 dependencies(此处未展示)。

代码完成后,开发者发布 dex_aggregator module。在发布期间,VM:

  1. 验证每个 module 都具有格式正确的 bytecode。
  2. 遍历已部署合约的所有传递依赖:0x123::dex_a0x123::dex_a 的所有传递依赖、0x456::dex_b0x456::dex_b 的所有传递依赖、0x1::fungible_asset 及其传递依赖;
  3. 对每个 dependency 收取 gas*,同时对遍历到的传递依赖数量施加硬性限制;
  4. 验证已部署的 module 能够正确链接到其 dependencies。

代码部署后,用户与其交互,提交交易来执行 swap。对于每笔交易,在执行 swap 之前,VM 都会重复发布流程中的步骤 (1) 和 (2):遍历传递依赖、收取 gas,并强制执行依赖数量的硬性限制。只有完成这些之后,它才会执行指定的 swap 函数。

系统看起来运行良好,用户也很满意,直到……

问题

使用 eager loading 时,随着 DEX aggregator module 或其 dependencies 的演进,问题会出人意料地容易出现。

问题 1:该 module 无法重新发布

开发者决定集成一个新的 exchange,发布为 dex_c。他们向 dex_aggregator module 添加 dex_c dependency,并尝试重新发布。结果发布失败,因为 transitive dependencies 的数量超过了硬性限制。这形成了可组合性的上限:无法再添加新的 exchange。

问题 2:该 module 无法加载

Exchange A 的开发者发布了一个新特性并升级了他们的 module dex_a。升级成功了;然而,dex_a 也增加了更多传递依赖,并因此破坏了 dex_aggregator:其传递依赖数量超过了限制。结果,任何对 aggregator 的调用都会失败,即使用户想通过未发生变化的 dex_b 来进行交换。

问题 3:过高的 gas 收费

即使 dex_aggregator 仍然可以调用,eager loading 也会增加执行成本。如果用户选择通过 exchange B 进行交换,则会调用 dex_b::swap,而不会触及 dex_a::swap。但 eager loading 仍然会对包含 dex_a(以及它新增的任何依赖)的所有传递依赖进行 metering。用户实际上是在为他们没有使用的集成支付 dependency tax

问题 4:性能开销

Loader V2 和 module caching 使频繁使用的 modules 加载速度非常快。但 eager loading 仍然需要遍历传递依赖来收取 gas 费用。即使所有 modules 都已经加载并位于 cache 中,VM 仍然必须执行与可能依赖数量成比例的工作,而不是与实际执行到的 dependencies 数量成比例的工作,这在依赖数量增长时会成为不必要的开销。

解决方案

这一解决方案作为 AIP-127 的一部分,于 2025 年 12 月 16 日在 Aptos mainnet 上启用,用 Lazy Loading 取代了 eager loading。Lazy Loading 将思维模型改为“只加载执行时实际用到的 modules”。

具体来说:

  • 当执行一个 entry function 或 script 时,VM 会在 modules 被访问时对其进行 metering 和加载。以上图 1 的示例为例,在 DEX A 上进行一次 swap 时,只会对 0x99::dex_aggregator0x123::dex_a 以及 0x123::dex_a 将要用到的任何 dependencies 收费。
  • 当发布一个 Move package 时,VM 会对已发布 package 中的所有 modules(包括正在升级的新版本和旧版本)以及它们的 immediate dependencies(而不是整个传递闭包)进行 metering。

以上图 1 的示例为例,在发布包含 DEX aggregator 代码的 module 时,VM 只会对 0x99::dex_aggregator0x123::dex_a0x456::dex_b 进行 metering。

这种 lazy 方法保留了所有安全保证:在发布时,每个 module 都会被验证并检查,以确保它能够正确链接到 dependencies。与此同时,这种方法移除了加载所有传递依赖这一不必要的要求。关于 module metering 的详细规范,请参见 AIP-127

对 Gas 使用的影响

使用 Lazy Loading 后,每笔交易的 gas 使用会降低**,因为 gas 只会对实际使用到的 modules 收费。同时,也更不容易触及 dependency limits。图 2 展示了在使用 Eager 或 Lazy Loading 回放 mainnet blocks 时的 block gas usage。该图显示了版本 [2719042368, 2719042916) 的 mainnet transactions 的 block gas usage,总计 61 个 blocks。\ \ \ \ 图 2:在使用 eager 或 lazy loading 回放 61 个 mainnet blocks [2719042368, 2719042916) 时的 block gas usage。越小越好。\ \

对性能的影响\

\ lazy loading 的目标并不是提升性能。尽管如此,实验评估显示,lazy loading 的性能优于或不逊于 eager loading。图 3 展示了在相同版本 [2719042368, 2719042916) 下,启用和不启用 lazy loading 时 mainnet blocks 的平均执行时间。前几个 blocks 的执行之所以更快,是因为发生的 module accesses(cache misses)更少。一旦 cache 中包含了大部分用到的 contracts,lazy loading 就能提供同等性能。\ \ \ \ 图 3:在使用 eager 或 lazy loading 回放 61 个 mainnet blocks [2719042368, 2719042916) 时的平均 block execution time(毫秒)。越小越好。\ \

结论\

\ eager loading 使 Move smart contract 的可组合性变得非常脆弱:contract upgrades 可能会因 dependency limits 而被阻止,或者在下游 dependency 发生变化后导致 transactions 失败。用户也会因为执行 transaction 时从未触及的 dependencies 而被收取 gas。\ \ Lazy loading(AIP-127)改变了这一模型:dependencies 会在首次使用时进行 metering 和加载。这对于 DeFi 尤其重要,因为 smart contracts 天生依赖很多其他 protocols:它们会针对许多其他 protocols 进行编译,但典型的 transaction 只会经过少数几条执行路径(例如某个特定 exchange)。使用 Lazy Loading 后,添加新的 dependency 不再会迫使每笔 transaction 为其所有传递依赖付费。下游 dependency upgrades 也不太可能破坏那些从未触及新代码路径的无关执行。\ \ *位于 0x1–0xf 等“特殊”地址上的 dependencies 不会被 metering。\ \ **在某些情况下,使用 lazy loading 可能会导致 gas usage 增加,仅仅因为此前从未对其进行 metering。例如,view functions 现在会对其加载的 modules 进行 metering。

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

0 条评论

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