Skip to main content

概要

尽快执行每个 EVM 合约调用直至完成是 Monad 整体性能的关键部分。为此,Monad 同时使用高度优化的解释器和定制的原生代码编译器。编译器一次分析常用合约并缓存原生代码,以便后续调用更高效地执行,同时保留精确的 EVM 语义(包括 gas 和错误行为)。

解释 vs. 编译

大多数以太坊客户端每次执行一条智能合约代码指令,在应用指令语义之前检查栈边界和可用 gas。这就是 解释器。解释器构建和维护简单,启动延迟低,并且当使用现代技术实现时可以表现得非常好。 另一种方法是 编译 程序。在执行开始之前,代码被分析并转换为更高效执行的表示形式。编译增加了前期延迟和复杂性,但每个合约版本只发生一次。如果重复执行更快,整体系统性能就会提高。 为了简单性和可移植性,许多编译器针对更高级别的 中间表示(例如 LLVM IRCranelift)。因为 Monad 客户端针对特定的硬件配置,编译器直接生成原生 x86-64,以最大化控制和性能,同时仍然精确匹配 EVM 行为。

消除冗余工作

编译使我们能够提前预计算行为。考虑这个直线代码片段:
一旦执行到达 JUMPDEST,它必须依次经过 PUSH1ADDPUSH0JUMP。纯解释器会为每个执行的指令收取 gas 并进行检查。编译器可以识别直线块,并对该块的组合成本执行一次前期 gas 检查(受 EVM 规则约束),然后生成运行该块的代码,而无需每条指令的记账。结果是较少的 CPU 指令,同时保持相同的 gas 计算和 out-of-gas 语义。 常量折叠是另一个例子。给定:
编译器可以确定 ADD 处的栈状态并将其折叠为:
内部处理,同时仍然收取与原始序列相同的总 gas,并保持 256 位模块化算术语义。

优化代码

除了消除冗余工作,编译器还根据操作数所在位置选择高效的实现。它维护一个 模拟 EVM 栈,将每个 256 位栈字映射到机器上的一个位置:主内存、一组通用整数寄存器,或一个 AVX 向量寄存器。这种方法结合了传统优化编译器中的寄存器分配和编译基于栈的语言的栈缓存技术。 然后,每个 EVM 指令针对其看到的操作数位置进行专门化。例如,当两个参数都已在 AVX 寄存器中时,AND 可以用单个 x86 vpand 实现。编译器的大部分有效性来自于为常见的操作数-位置组合发出专门的序列。

编译性能

提前编译每个合约是不切实际的。相反,编译器跟踪合约在所有执行中累积消耗的 gas,并为”最热”的合约缓存原生代码。随着区块执行,新变热的合约进入编译队列。编译异步运行,尚未编译(或从未变热)的合约继续在高度优化的解释器上运行。缓存确保对流行合约的重复调用受益于编译,而不会因编译延迟而阻塞执行。