第16节:节约gas
小白入门:https://github.com/dukedaily/solidity-expert ,欢迎star转发,文末加V入群。
职场进阶: https://dukeweb3.com
- 使用calldata替换memory
- 将状态变量加载到memory中
- 使用++i替换i++
- 对变量进行缓存
- 短路效应
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;
// gas golf
contract GasGolf {
// start - 50908 gas
// use calldata - 49163 gas
// load state variables to memory - 48952 gas
// short circuit - 48634 gas
// loop increments - 48244 gas
// cache array length - 48209 gas
// load array elements to memory - 48047 gas
uint public total;
// start - not gas optimized
// function sumIfEvenAndLessThan99(uint[] memory nums) external {
// for (uint i = 0; i < nums.length; i += 1) {
// bool isEven = nums[i] % 2 == 0;
// bool isLessThan99 = nums[i] < 99;
// if (isEven && isLessThan99) {
// total += nums[i];
// }
// }
// }
// gas optimized
// [1, 2, 3, 4, 5, 100]
function sumIfEvenAndLessThan99(uint[] calldata nums) external {
uint _total = total;
uint len = nums.length;
for (uint i = 0; i < len; ++i) {
uint num = nums[i];
if (num % 2 == 0 && num < 99) {
_total += num;
}
}
total = _total;
}
}
十种节约方法
一共两种类型的Gas需要消耗,他们是:
- 调用合约
- 部署合约
有时候,减少了一种类型的gas会导致另一种类型gas的增加,我们需要进行权衡(Tradeoff)利弊,主要优化方向:
- Minimize on-chain data (events, IPFS, stateless contracts, merkle proofs) -> 优化链上存储
- Minimize on-chain operations (strings, return storage value, looping, local storage, batching) -> 优化链上操作
- Memory Locations (calldata, stack, memory, storage) -> 数据位置的选择
- Variables ordering -> 变量的定义顺序很重要
- Preferred data types -> 数据类型的选择
- Libraries (embedded, deploy) -> 尽量使用库来减少部署gas
- Minimal Proxy -> 使用clone方式创建新合约
- Constructor -> 优化构造函数(尽量使用constant)
- Contract size (messages, modifiers, functions) -> 优化合约size
- Solidity compiler optimizer -> 开启优化中
1. Minimize on-chain data
- Event:如果链上合约不需要调用的数据,可以使用event,由链下监听,提供只读操作;
- IPFS:大数据可以上传到ipfs,然后将对应的id存储在链上;
- 无状态合约:如果只是为了存储key-value,那么在合约中不需要状态变量存储,而是仅仅通过参数记录,让链下程序去解析交易,读取参数,从而读取到key-value数据;
- 默克尔根(Merkle Proofs):快速验证数据,合约不用存储太多内容。
2. Minimize on-chain operations
- string:string内在也是bytes,尽量使用bytes替代,可以减少EVM计算,减少gas消耗;
- 返回storage值:直接返回storage如果有必要的话,具体内部数据,让链下程序解析;
- Local Storage:使用local storage变量,可节约开销,不要使用memory进行copy一遍操作;
- Batching(批量操作):如果有批量操作需要,可以提供相应接口,避免用户发起相同交易。
3. Memory locations
四种存储位置gas消耗(由低到高):calldata -> stack -> memory -> storage.
- Calldata:一般用在参数中,修饰引用数据类型(array、string),限定external function,尽量使用,便宜;
- Memory:对于存储引用类型的数据时,完全拷贝(你没有看错,反而更便宜)比storage便宜;
- Storage:最贵,非必要,不使用。
- Stack:函数体中值类型的数据,自动修饰为stack类型;
4. Variables ordering
- Storage slots(槽)大小是32字节,但并不是所有的类型都能填满(bool,int8等);
- 调整顺序,可以优化storage使用空间:
- uint128、uint128、uint256,一共使用两个槽位(good)✅
- uint128、uint256、uint128,一共使用三个槽位(bad)❌
5. Preferred data types
- 如果定义变量的类型原本可以填满整个槽位,那么就填满ta,而不要使用更短的数据类型。
- 例如:如果定义数据类型:datatype:uint8,但是opcode原则上是处理:uint256的,那么会对空余部分填充:0,这反而会增加evm对gas的开销,所以更好的方法是:直接定义datatype为:uint256。
6. Libraries
库有两种表现形式:
- Embedded Libraries:当lib中的方法都是internal的时候,会自动内联到合约中,此时对节约gas不起作用;
- Deployed Libraries:当lib中有public或external方法时,此时会单独部署lib合约,我们可以使用一个lib地址关联到不同合约来达到节约gas的目的。
7. Minimal Proxies (ERC 1167)
- 这是一个标准,用于clone合约
- openzeppelin合约中clone就源于此
8. Constructor
- 构造函数中可以传递immutable数据,如果可能,尽量使用constant,这样开销更小。
9. Contract Size
- 合约最大支持24K
- 减少Logs/ Message:require后面的des,event的使用,都影响合约size
- 使用opcode:这个看情况而定,opcode可能减少部署开销,却引来调用开销的增加。
- 修饰器Modifier:modifer中wrapped一个函数,在函数中实现具体逻辑 // TODO
10. Solidity compiler optimizer
开启编译器optimize,这个是有双面性的,一定会使得合约size变小;
但是可能会使部分函数的逻辑变复杂(code bigger),增加函数的执行开销。
Solidity Gas Optimizations Tricks
1. 使用最新版本solidity
EVM的升级会带来gas的优化
2. for循环优化
// 避免重复计算长度
uint length = arr.length;
// ++i 可以减少一次赋值,初始化i放在for里面
for (uint i; i < length;) {
// unchecked可以减少溢出校验
unchecked { ++i; }
}
3. 使用calldata代替memory
calldata是存储不可修改的函数参数的位置,仅支持external函数,使用calldata可以减少一次将参数复制到memory的操作。
从calldata中加载使用:calldataload,从memory中加载使用:mload
4. 尽量使用immutable代替state variable
EIP-2929 对slot的操作引入了cold slot(消耗2100 gas)和warm slot(消耗100 gas)的概念,但是依然很贵,而修饰为immutable的变量会保存在bytecode中,使用时使用的是push,仅消耗3gas
5. 使用immutable代替constant(计算keccak时)
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
contract Immutables is AccessControl {
uint256 public gas;
bytes32 public immutable MANAGER_ROLE_IMMUT;
// 每次使用都需要重新计算hash值
bytes32 public constant MANAGER_ROLE_CONST = keccak256('MANAGER_ROLE');
constructor(){
// 仅在构造时计算hash一次
MANAGER_ROLE_IMMUT = keccak256('MANAGER_ROLE');
_setupRole(MANAGER_ROLE_CONST, msg.sender);
_setupRole(MANAGER_ROLE_IMMUT, msg.sender);
}
function immutableCheck() external {
gas = gasleft();
require(hasRole(MANAGER_ROLE_IMMUT, msg.sender), 'Caller is not in manager role'); // 24408 gas
gas -= gasleft();
}
function constantCheck() external {
gas = gasleft();
require(hasRole(MANAGER_ROLE_CONST, msg.sender), 'Caller is not in manager role'); // 24419 gas
gas -= gasleft();
}
}
6. 使用modifier代替function
经过对比,modifier合约的foo方法更加节约gas
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
contract Inlined {
function isNotExpired(bool _true) internal view {
require(_true == true, "Exchange: EXPIRED");
}
function foo(bool _test) public returns(uint) {
isNotExpired(_test);
return 1;
}
}
contract Modifier {
modifier isNotExpired(bool _true) {
require(_true == true, "Exchange: EXPIRED");
_;
}
function foo(bool _test) public isNotExpired(_test)returns(uint) {
return 1;
}
}
7. modifier中使用internal函数减少合约size
尽量将modifier中的条件判断逻辑写在单独的internal view函数中,可以减少合约size,从而减少gas消耗。
以下代码中,onlyOwner在多处使用,使用越多节约效果越明显
pragma solidity ^0.8.10;
contract Context {
function _msgSender() internal view returns(address) {
return msg.sender;
}
}
contract Ownable is Context {
address public owner = _msgSender();
modifier onlyOwner() {
require(owner == _msgSender(), "Ownable: caller is not the owner");
_;
}
}
contract Ownable2 is Context {
address public owner = _msgSender();
modifier onlyOwner() {
_checkOwner();
_;
}
function _checkOwner() internal view virtual {
require(owner == _msgSender(), "Ownable: caller is not the owner");
}
}
// This is deployment gas cost for each function
// 0: 107172
// 1: 145772
// 2: 181610
// 3: 198170
// 4: 214532
// 5: 241059
contract T1 is Ownable {
event Call(bytes4 selector);
function f0() external onlyOwner() { emit Call(this.f0.selector); }
function f1() external onlyOwner() { emit Call(this.f1.selector); }
function f2() external onlyOwner() { emit Call(this.f2.selector); }
function f3() external onlyOwner() { emit Call(this.f3.selector); }
function f4() external onlyOwner() { emit Call(this.f4.selector); }
}
// 0: 107172
// 1: 147908
// 2: 165818
// 3: 183506
// 4: 192500
// 5: 211682
contract T2 is Ownable2 {
event Call(bytes4 selector);
function f0() external onlyOwner() { emit Call(this.f0.selector); }
function f1() external onlyOwner() { emit Call(this.f1.selector); }
function f2() external onlyOwner() { emit Call(this.f2.selector); }
function f3() external onlyOwner() { emit Call(this.f3.selector); }
function f4() external onlyOwner() { emit Call(this.f4.selector); }
}
验证结果:
8. >= is cheaper than >
可以减少内部校验是否为0的逻辑
Non-strict inequalities (>=
) are cheaper than strict ones (>
). This is due to some supplementary checks (ISZERO
, 3 gas)).
uint256 public gas;
function checkStrict() external {
gas = gasleft();
require(999999999999999999 > 1); // gas 5017
gas -= gasleft();
}
function checkNonStrict() external {
gas = gasleft();
require(999999999999999999 >= 1); // gas 5006
gas -= gasleft();
}
9. 使用SHR和SHL来代替乘除法
DIV使用5gas,SHR使用3gas,而且后者不用额外校验除数为0的逻辑,更加节约gas
10. 使用多个require代替使用&&
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
import "hardhat/console.sol";
contract Requires {
uint256 public gas;
function check1(uint x) public {
gas = gasleft();
console.log(gasleft());
require(x == 0 && x < 1 ); // gas cost 22156
console.log(gasleft());
gas -= gasleft();
}
function check2(uint x) public {
gas = gasleft();
console.log(gasleft());
require(x == 0); // gas cost 22148
require(x < 1);
console.log(gasleft());
gas -= gasleft();
}
}
11.使用自定义error代替revert("err info")
https://blog.soliditylang.org/2021/04/21/custom-errors/
语义更加丰富,同时节约gas,高效
12. public/private/internal/external等
在调用层面,四个修饰符gas fee没有不同之处
但是在部署的时候,public币private和internal更贵一些
13. 对状态变量进行caching节约gas
一个状态变量如果在一个函数中会被使用多次,那么使用memory进行caching可以节约gas
SLOAD:100gas
MLOAD:3 gas
14. 对结构体格外注意
// SPDX-License-Identifier: MIT
pragma solidity 0.8.9;
import "hardhat/console.sol";
contract Requires {
event Unlock(address _sender, uint256 _nftIndex, uint256 _amt);
// struct LockPosition use 3 slots
mapping(uint256 => LockPosition) positions;
struct LockPosition {
address owner;
uint256 unlockAt;
uint256 lockAmount;
}
function unlock(uint256 _nftIndex) external {
// 3 SLOADs and 3 MSTORES
// 从storage读取三个变量,并且复制到memory中
LockPosition memory position = positions[_nftIndex]; // gas: costing 3 SLOADs while only lockAmount is needed twice.
//Replace "memory" with "storage" and cache only position.lockAmount
require(position.owner == msg.sender, "unauthorized");
require(position.unlockAt <= block.timestamp, "locked");
delete positions[_nftIndex];
payable(msg.sender).transfer(position.lockAmount);
emit Unlock(msg.sender, _nftIndex, position.lockAmount);
}
function unlockOptimize(uint256 _nftIndex) external {
LockPosition storage position = positions[_nftIndex];
uint256 amt = position.lockAmount;
//Replace "memory" with "storage" and cache only position.lockAmount
require(position.owner == msg.sender, "unauthorized");
require(position.unlockAt <= block.timestamp, "locked");
delete positions[_nftIndex];
payable(msg.sender).transfer(amt);
emit Unlock(msg.sender, _nftIndex, amt);
}
}
15. 复用非零值的状态变量更节约gas
Writing to an Existing Storage Slot Is Cheaper Than Using a New One
https://eips.ethereum.org/EIPS/eip-2200
EIP — 2200 changed a lot with gas, and now if you hold 1 Wei of a token it’s cheaper to use the token than if you hold 0. There is a lot to unpack here so just google EIP 2200 and learn if you want, but in general, if you need to use a storage slot, don’t empty it if you plan to refill it later.