缓存数据
分析
在高效的 Solidity 代码中,你会经常看到以下模式。由于 Solidity 不会缓存存储读取操作,因此读取存储变量至少需要 100 Gas。写入操作的成本要高得多。因此,你应该手动缓存变量,以便只执行一次存储读取和一次存储写入操作。
场景1:缓存存储数据
在 EVM 中,读取存储变量使用的是 SLOAD 操作码,每次读取大约消耗 100 Gas。而读取内存变量使用的是 MLOAD 操作码,每次仅消耗约 3 Gas。因此,将存储变量的>值读取一次并缓存到内存中,可以在后续的操作中使用内存变量,显著减少 Gas 消耗。
例如使用 for 循环迭代数组而不缓存数组长度,则每次迭代都会从存储中读取长度,这会导致每次读取操作产生额外的 Gas 成本。可以在循环执行之前将数组长度缓存到变量中,可以最大限度地减少读取操作的次数,从而每次迭代节省大约 3 个 Gas 单位。这可以显著节省 Gas,尤其是在循环遍历大型数组的情况下。
场景2:缓存外部调用
缓存外部合约调用的结果是一项非常有效的 Gas 节省技巧,尤其是在您需要在函数中多次使用检索到的数据时。
-
外部调用成本:每次调用外部合约都会消耗大量的 Gas。如果需要多次使用外部调用的结果,每次调用都会造成效率低下且成本高昂。
-
缓存的好处:通过将外部调用的结果缓存在局部变量中,您可以重复使用这些数据,而无需承担多次外部调用的成本,从而节省gas。
有时,开发者会将值缓存到变量中以供后续使用,但这并非总是必要的,甚至可能导致 Gas 消耗增加。
-
增加 Gas 成本:创建一个变量来存储仅使用一次的数据可能会导致 Gas 成本增加,而不会提高可读性或功能性。
-
避免冗余缓存:注意不必要的变量赋值,尤其是在处理结构体属性或映射时。如果值仅使用一次,则直接访问而不进行缓存会更节省 Gas。
测试
文件位置:
src/storage/Cache.sol
test/storage/Cache.t.sol测试结果:
forge test --gas-report --mt test_cache_ -vvv --optimize
在 unoptimized 函数中,counter 被读取了三次,每次都消耗约 100 Gas。而在 optimized 函数中,counter 仅被读取一次并缓存到内存中,后续操作使用内存变量 _counter,每次读取仅消耗约 3 Gas,从而显著减少了总的 Gas 消耗 。
总结
-
审查您的智能合约:检查您的合约中是否存在不必要地将值缓存到变量中的情况。
-
直接访问值:如果缓存值仅使用一次,请考虑直接访问它而不是将其存储在变量中。
-
缓存频繁访问的存储变量:在函数中多次访问同一个存储变量时,建议将其值缓存到内存中,减少对存储的重复读取。
-
避免不必要的存储写入:在更新存储变量之前,先检查新值是否与当前值不同,只有在值发生变化时才执行写入操作,以节省 Gas。
-
注意结构体的缓存方式:对于结构体类型的存储变量,直接将整个结构体复制到内存中可能会导致额外的 Gas 消耗。建议仅缓存需要频繁访问的字段,或使用 storage 指针来操作结构体。
对应源码
本文配套代码固定到提交
8960383;Gas 数据会随编译器、优化器参数和 EVM 版本变化。