本文评估了 Arbitrum Stylus 相对于 EVM 在智能合约性能方面的改进。
我们的目标是评估 Arbitrum Stylus 相比 以太坊虚拟机 (EVM),在智能合约性能上有哪些提升。 为了实现这个目标,我们建议使用中心化限价订单簿(CLOB)产品作为案例。 这是一个真实的例子,需要大量的交易来进行做市,因此对性能非常敏感。
Arbitrum 是构建在以太坊之上的 Layer 2 链,被 l2beat.com 认为是 Rollup 第一阶段的领先链之一。 这种先进的 Rollup 阶段使得 Arbitrum 成为托管 CLOB 应用的最合适的 L2 解决方案,这些应用既具有高性能和成本效益,同时保持 EVM 兼容性(以太坊虚拟机)。
_根据 l2beat 得出的当前 L2 排名及其 Rollup 阶段_
Arbitrum 基金会最近推出了 Arbitrum Stylus,这是一个使开发者能够使用 Rust 编写与 EVM 兼容的智能合约的 SDK。 通过在 EVM 环境中利用 Rust 的性能能力,与 Solidity 智能合约相比,我们有可能获得更快,更经济的结果,因为 Rust 比 Solidity 在更低的层级上运行。
Arbitrum Stylus 声称在多个领域可以显著节省 Gas 费用,使其成为开发高性能和低成本智能合约的有吸引力的选择。
根据官方文档,内存成本降低 100x-500x,计算成本降低 10x-50x,并且潜在的存储成本也更低,Arbitrum Stylus 比 Solidity 智能合约具有显著的优势。 虽然在调用合约函数时存在较小的开销,范围在 128–2048 Gas 单位之间,但费用消耗的总体减少使 Stylus 成为开发优于基于 Solidity 的 CLOB 的 兼容 ERC20 的 CLOB 的一个有趣的选择。
A — 介绍
在本节中,我们将执行 Dexalot Solidity 智能合约到 Stylus 智能合约的朴素翻译,不进行任何优化。 我们的目标是确定 Stylus 是否能够在不需要任何特定优化的情况下实现所指示的性能优势。
B — 红黑树基准
中心化限价订单簿(CLOB)的核心在于其数据结构,通常基于红黑二叉树。 这种自平衡树确保订单始终以正确的顺序维护,最低价格作为其最左边的子节点,最高价格作为其最右边的子节点。
为了准确比较 Stylus 智能合约和 Solidity 智能合约的性能,我们将进行基准测试。 此测试将涉及使用这两种类型的合约来实现红黑二叉树并与之交互。 两个合约都将在 Arbitrum Stylus 上部署。 这种方法将使我们能够在中心化限价订单簿(CLOB)的背景下评估 Stylus 和 Solidity 之间的性能差异。
由于这主要是一个基于存储的合约,我们预计使用 Stylus 不会获得显著的性能提升。 但是,我们仍然希望看到 Gas 费用的减少或执行速度的提高。
比较 Solidity 与 Stylus 的 Gas 消耗和执行速度的图表。
在进行第一次基准测试后,我们观察到,在 Gas 消耗方面,Stylus 智能合约比 Solidity 智能合约更慢且更昂贵,以每次分批(150 个节点)插入 2000 个节点为例,平均 Gas 消耗高 25%。
为了更好地理解这种性能差距背后的原因,我们现在将仔细研究两个合约的代码,并分析导致差异的潜在原因。
一个关键的区别是在两种语言中对 Map 的使用,Solidity 和 Stylus 中对这些 Map 的 Read/Write 方法截然不同。
C — Map 插入基准
为了验证我们关于性能差异潜在原因的假设,我们需要仅对 Map 中元素的插入进行测试。
我们将在 Stylus 和 Solidity 中创建一个简单的函数,将一个元素添加到 Map 中,然后比较两种实现的 Gas 消耗。 这将帮助我们确定 Map 访问方法的差异是否确实是导致观察到的性能差距的原因。
fn batch_insert(&mut self, limit: U256) {
let mut start = U256::from(0)
while start < limit {
//let mut ref_index: stylus_sdk::storage::StorageGuardMut<'_, stylus_sdk::storage::StorageUint<256, 4>> = self.order_book_map.setter(start);
let mut ref_index: stylus_sdk::storage::StorageGuardMut<'_, stylus_sdk::storage::StorageUint<256, 4>> = self.order_book_map.setter(start);
ref_index.set(start)
start = start + U256::from(1);
}
}
在 Stylus 中,如果不使用 .setter() 和 .set() 方法,我们无法直接修改 Map 中的值。
function batchInsert(uint256 limit) external {
uint256 start = 0;
while (start < limit) {
test[start] = start;
start = start + 1;
}
}
比较 Solidity 与 Stylus 的 Gas 消耗的图表。
在使用简单函数将元素添加到 Map 中进行测试后,我们再次观察到,在 Gas 消耗方面,Stylus 合约比 Solidity 合约更昂贵。 乍一看,我们可能认为这种差异仅仅是由于每次函数调用时花费的开销 Gas 费用。
但是仔细观察,Stylus 和 Solidity 合约之间的 Gas 消耗差异不仅归因于开销 Gas 价格。 观察到的 Gas 消耗差异表明,还有另一个因素导致 Stylus 合约中更高的 Gas 消耗,导致差异超过了 2048 单位的最大开销 Gas 价格。 这表明两个合约之间的差异大于仅仅是开销 Gas 价格。
根据我们测试的观察结果,问题似乎确实与我们在 Stylus 合约中访问 Map 的方式有关。 与 Solidity 合约相比,在 Stylus 中使用 setter() 和 set() 方法似乎导致了更高的 Gas 消耗。
A — 介绍
既然我们已经确定从 Solidity 到 Stylus 的朴素翻译不能提供 Gas 节省或性能改进,现在是时候利用 Rust 的强大功能来优化 Stylus 合约中红黑二叉树的实现了。 通过使用 Rust 的底层控制和性能能力,我们有可能在 Gas 消耗和执行速度方面实现显著的改进。
B — 零拷贝实现
为红黑二叉树实现零拷贝方法的目标是仅加载一次树,使用引用,然后将更改保存回树。 这种方法确保在插入过程中不复制或复制插入到树中的数据。 相反,原始数据被直接引用并插入到树中,而不创建任何中间副本。
OpenBookV2 和 Phoenix 等著名的 CLOB 已经实现了零拷贝方法,它们在 Solana 区块链上运行并利用 Rust 智能合约。 通过避免不必要的数据复制,零拷贝策略可以帮助减少将新节点插入树所需的内存和处理量,从而提高性能和效率。
在基于 Phoenix 订单簿简单版本为红黑二叉树实现零拷贝方法后,我们在订单簿初始化阶段遇到了一个限制性问题。
我们发现初始化大小为 20 max 的树会导致过多的 Gas 消耗,在初始化过程中会消耗高达 1,700,000 Gas 单位。 更大的树大小导致合约崩溃。
##[external]
impl ClobTree {
pub fn init_market(&mut self) {
let bids = RedBlackTree::<FIFOOrderId, FIFORestingOrder, ORDERS_LEN>::new();
let market = FIFOMarket { bids };
let bytes = bytemuck::bytes_of(&market);
self.market_bytes.set_bytes(bytes);
}
}
问题在于 Stylus 提供的 .set_bytes() 函数,由于 EVM 一次只能处理 256 位的特性,它一次只能处理 32 个字节,这导致多次调用 .set() 函数,从而导致 Gas 费用飙升,而在 SVM(Solana 虚拟机)上,我们可以一次加载所有树。
我们发现,由于树初始化期间 Gas 消耗过多,使用 Phoenix 订单簿的简单版本的零拷贝策略对于在 Stylus 中实现红黑二叉树是不可行的。 Stylus 中可以初始化的最大树大小仅为 20,而 Phoenix 的树大小为 1048。
根据我们的基准测试结果,我们可以得出结论,由于 Stylus 智能合约处理 内存读写 的方式,它们目前可能不最适合 存储密集型 合约。 此外,我们发现,由于需要遵守 EVM 环境,Rust 原生方法(例如零拷贝策略)在 Stylus 上也受到限制。
我们的发现得到了 Stylus SDK 开发人员的证实,他们承认该项目仍处于早期阶段。 他们告知我们,计划进行进一步的开发,以解决 仅涉及存储的智能合约的 Gas 消耗问题。
- 原文链接: medium.com/swissborg-eng...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!