以太坊上存储256 bit数据大约消耗20k Gas、如此换算,仅1 GB存储资源要花费32,000ETH,大约要花费超过1亿美元。且不说当前身为贵族链Gas费很有可能继续水涨船高,放在早些年其Gas消耗也不是一笔小数目。因此,以太坊Gas优化是Dapp开发一直难绕的问题,也是Solidity开发者的必备技能。
查看交易花费的总gas和价格,交易详情中直接查看
查看交易trace,在交易详情中点击Parity Trace, 可以看到每一个内部交易的gas,主要是call, delegatecall等
选择geth Trace 则可以看到Opcode层面的gas消耗情况
remix中主要在交易执行结果console中查看。
Transaction Cost 基于将数据发送到区块链的成本。全部交易成本由 4 项组成:
Execution Cost 基于作为交易结果而在EVM执行的计算操作的成本。
仅使用remix内置的debug链能输出区分Transaction Cost,Execution Cost 如果连接其他的链只能获得总gas
hardhat不会直接给出gas情况,在执行部署合约以及合约交互时一般可以通过promise中的交易hash获取回执,从回执中得到结果。
合约交互的交易:
let res = await contract.mint(user.address, 10000);
let receipt = await hre.ethers.provider.getTransactionReceipt(res.hash);
console.log("gas used: ", receipt.gasUsed);
console.log("gas*price: ", receipt.gasUsed.mul(receipt.effectiveGasPrice));
部署合约的交易:
let res = await contract.deployed();
let receipt = await hre.ethers.provider.getTransactionReceipt(
res.deployTransaction.hash
);
console.log("gas used: ", receipt.gasUsed);
console.log("gas*price: ", receipt.gasUsed.mul(receipt.effectiveGasPrice));
let contractFactory = await hre.ethers.getContractFactory("ActivityToken");
let contract = await contractFactory.deploy("ActivityToken", "AT");
// 预估部署gas,不是很准确,暂时没有更好的方法
console.log(
"Deploy Estimated gas:",
await ethers.provider.estimateGas(contractFactory.bytecode)
);
let res = await contract.deployed();
// 预估调用合约的gas
console.log(
"Mint estimated Gas: ",
await contract.estimateGas.mint(deployer.address, 100000)
);
在hardhat中主要是使用hardhat-gas-reporter插件,可以在运行单元测试时,同时生成执行的gas报告。报告中可以看到测试过程中每个函数的平均gas消耗以及,部署过程中的gas消耗。
Hardhat Gas Reporter 是一个 Hardhat 插件,可以用来在控制台中显示每个合约函数的 gas 使用量,以及整个合约的 gas 使用量。下面是使用 Hardhat Gas Reporter 的步骤:
npm install --save-dev hardhat-gas-reporter
require("hardhat-gas-reporter");
module.exports = {
gasReporter: {
currency: 'CHF',
gasPrice: 21
}
}
效果如下:
gasReporter: {
//doc:https://github.com/cgewecke/eth-gas-reporter
enabled: true,
currency: "USD", //默认EUR, 可选USD, CNY, HKD
// 默认token是ETH, 更换成其他的会实时在coinmarketcap找价格
// token: "MATIC",
coinmarketcap: "59a52916-XXXX-XXXX-XXXX-2d6f56917aee", //https://coinmarketcap.com/
// 默认从 eth gas station api 中获取eth价格,其他代币可自行输入gasPrice(推荐), 或填入gasPriceAPI(注意调用限制)
// gasPrice: 30,
// gasPriceApi:"https://api.etherscan.io/api?module=proxy&action=eth_gasPrice",
},
https://www.npmjs.com/package/hardhat-gas-reporter
基础GAS计算公式:
gas = txGas + dataGas + opGas
如果交易没有创建新的合约,则txGas为 21000,否则txGas为 53000。交易中data的每个零字节需要花费 4 个 gas,每个非零字节需要花费 16 个 gas。opGas是指运行完所有的 op 所需要的 gas。
一般来说opGas的优化空间更大。
合约gas消耗:
在评估gas时,往往要在上述二者间进行折中。
不同操作使用的gas
Operation Gas Description
ADD/SUB 3 Arithmetic operation
MUL/DIV 5 Arithmetic operation
ADDMOD/MULMOD 8 Arithmetic operation
AND/OR/XOR 3 Bitwise logic operation
LT/GT/SLT/SGT/EQ 3 Comparison operation
POP 2 Stack operation
PUSH/DUP/SWAP 3 Stack operation
MLOAD/MSTORE 3 Memory operation
CALLDATALOAD 3 Calldata operation
JUMP 8 Unconditional jump
JUMPI 10 Conditional jump
SLOAD 100/2100 Storage operation (热访问/冷访问)
SSTORE 5,000/20,000 Storage operation
BALANCE 400 Get balance of an account
CREATE 32,000 Create a new account using CREATE
CALL 25,000 Create a new account using CALL
KECCAK256 gas_cost = 30 + 6 * data_size_words + mem_expansion_cost
LOG gas_cost = 375 + 375 * num_topics + 8 * data_size + mem_expansion_cost
详见:https://ethereum.org/en/developers/docs/evm/opcodes/
目前solidity有两种优化器:
参考:
https://docs.soliditylang.org/zh/v0.8.19/internals/optimizer.html 优化器文档
https://docs.soliditylang.org/zh/v0.8.19/yul.html# Yul语言文档
https://learnblockchain.cn/article/6064 Yul入门指南
使用solc命令时:
目前 --optimize 启动了opcode-based optimizer用于bytecode优化, 同时启动Yul optimizer用于内部生成的Yul code的优化.
使用 solc --ir-optimized --optimize 可以生成Solidity源码对应的 optimized Yul IR .
使用 solc --strict-assembly --optimize 是启用专门的 Yul 模式优化.
使用hardhat时:
插件仅支持配置optimizer一个选择 ,即不能选择ir-optimized等选项,目前都是默认优化模式。
module.exports = {
solidity: {
version: "0.8.9",
settings: {
optimizer: {
enabled: false,
runs: 200,
},
},
},
};
运行次数( --optimize-runs )大致规定了在合约有效期(可以理解为1年)内, 所部署的代码的每个操作码被执行的频率。 这意味着它是代码大小(部署成本)和代码执行成本(部署后的成本)之间的一个折衷参数。 一个 “运行” 参数为 “1” 将产生简短的合约但昂贵的执行代码。相反, 一个较大的 “运行” 参数将产生较大的合约但更省gas的执行代码。 该参数的最大值为 2^32-1。
注意runs不是越大越好,也不是指运行多少次优化迭代
一种简单的理解是runs是是否要内联的启发式参数, runs越多,就越倾向于内联。
uint256 public v1;
uint256 public constant v2 = 1000;
function calculate() returns (uint256 result) {
return v1 * v2 * 10000
}
此时v2,10000都是bytecode中的一员, 而v1是状态变量中的一员。
每次读取v1需要额外执行一次sload, 将花费200gas。
对于external:
public修饰等于external+internal , 如果仅指定extenral,这个函数的参数不需要存储在内存中,而是直接从calldata中读取, 相反如果public函数的参数就要存入内存中。
因此,当可以使用external时,不要使用public.
这个优化方式对于参数比较大的函数尤为有效。
对于view, pure:
view不会改变区块链上任何状态,但要注意只有external view函数或者public view函数被外部调用时才是免费的, 在交易中被调用任然需要正常扣费。 pure的情况类似。
除非需要打包数据或者进行迭代,否则映射的成本更低。 使用映射也可以通过数字作为key索引的形式来迭代。
如果逻辑明确了数组长度,使用定长数组也可以。
如果不改变参数内容,仅仅是读取数据, 优先指定为calldata。calldata的读取消耗是和memory基本一致的, 只是所有的入参本身作为calldata就已经占据了一份存储, 如果指定为memory的话还会将calldata数据拷贝如memory中存下其index,会多花费这一部分多出的资源。
// calldata
function func2 (uint[] calldata nums) external {
for (uint i = 0; i < nums.length; ++i) {
...
}
}
// Memory
function func1 (uint[] memory nums) external {
for (uint i = 0; i < nums.length; ++i) {
...
}
}
此外,对于状态变量,可以尽量减少其在循环中被反复使用
//优化后
uint sum = 0;
function p3 ( uint x ){
for ( uint i = 0 ; i < x ; i++)
sum += i; }
//优化后
uint sum = 0;
function p3 ( uint x ){
uint temp = 0;
for ( uint i = 0 ; i < x ; i++)
temp += i; }
sum += temp;
//Using delete keyword
delete myVariable;
//Or assigning the value 0 if integer
myInt = 0;
//下列动作会清理状态变量中该数据原有的值;
myAddresses = new address[](0);
什么是gas refund 1. 合约调用的 selfdestruct 将合约销毁或者调用 sstore 将状态变量的值由非空变为空都可以得到 gas 退回。 2. gas 退回并不意味着账户里的以太币余额会增加,只是意味着这次交易所花费的 gas 量会减少。gas 退回有个上限,就是不能超过当前交易所花费 gas 的 50%,如果超过,就按 50% 算。 3. gas 退回也不意味着交易发起者账户里的余额可以少一点儿,余额校验仍然是基于 gas price * gas limit。
单个变量尽量都使用unit256, 因为EVM单次操作32字节, 对于uint8和bool的数据还要进行填充,需要更多gas。(多用uint256也少很多类型转换和兼容性问题)
只有在连续存储时,特别是放入结构体中时,使用小结构体才是有效的,此时需要注意内存对齐的顺序。
contract Leggo {
uint128 a;
uint128 c;
uint256 b;
}
理解:什么是变量清理
清理变量(Cleaning Up Variables): 当一个值的占用位数小于32个字节,其中无用的位将会被清除。无论是加载到内存中或者是在存储中,都会这样做, 否则会影响计算hash或者生成calldata之类的逻辑。
https://docs.soliditylang.org/zh/v0.8.16/internals/variable_cleanup.html
同理 byte[] 的效率也因为清理变量的原因比较低下,尽量可以使用bytes32或者其他bytesX代替。
同理,对于string如果其长度低于32, 也可以考虑使用byteX代替。
// 599 gas
function useString() public returns(string memory a) {
a = "hello world!";
}
// 196 gas
function useByte() public returns(bytes32 a) {
a = bytes32("hello world!");
}
替换require(isOwner(msg.sender), "Unauthorized")为 if (!isOwner(msg.sender)){revert Unauthorized();} 减少revert信息.
由于函数签名的不同,调用函数时,EVM需要帮你查找函数,因此按照函数签名的数值大小,先找到的花费的gas就更少,后找到的花费gas就更多,因此对于调用特别频繁的函数,我们可以考虑调整其优先级。 另外要注意,public状态变量,也参与查找计算(即uint256 public a 有一个 a() 函数)。
结论:
参考:
在有多个input参数时, 可以看见组织input data的时候是32byte为一组的。
Function: trade(address tokenGet, uint256 amountGet, address tokenGive, uint256 amountGive, uint256 expires, uint256 nonce, address user, uint8 v, bytes32 r, bytes32 s, uint256 amount) ***
MethodID: 0x0a19b14a
[0]:0000000000000000000000000000000000000000000000000000000000000000
[1]:000000000000000000000000000000000000000000000000006a94d74f430000
[2]:000000000000000000000000a92f038e486768447291ec7277fff094421cbe1c
[3]:0000000000000000000000000000000000000000000000000000000005f5e100
[4]:000000000000000000000000000000000000000000000000000000000024cd39
[5]:00000000000000000000000000000000000000000000000000000000e053cefa
[6]:000000000000000000000000a11654ff00ed063c77ae35be6c1a95b91ad9586e
[7]:000000000000000000000000000000000000000000000000000000000000001c
[8]:caa3a70dd8ab2ea89736d7c12c6a8508f59b68590016ed99b40af0bcc2de8dee
[9]:26e2347abfba108444811ae5e6ead79c7bd0434cf680aa3102596f1ab855c571
[10]:000000000000000000000000000000000000000000000000000221b262dd8000
这个时候我们可以利用里面的空间,把多个像uint8, address这样不会占据全部空间的参数组合在一起,变成一个uint256, 在合约内部在通过mload解析开数据。
对于inputdata中,每多一个byte,会增加68gas(byte是全0则增加4gas), 对于频繁发生的调用,压缩inputdata是有必要的。
判断时低成本的判断先做(短路模式,Short-circuiting rules): 如 f(x) || g(y) 应该让更容易判断为true的条件放在前边。
降低不必要的依赖。
避免在循环中做高消耗的动作,合并可以合并的循环, 提取循环不变的表达式到外部,循环中避免直接累加状态变量,避免在循环中多次调用arr.length。
++i 优于 i++ 优于 i+=1。
对于确定的计算,可以使用uncheck块: require(a <= b); unchecked { x = b - a }
主要是对于library的复用, 一般来说对于库文件的internal调用会让调用花费更少,但由于库的嵌入和内联会导致部署时的花费增多; 而调用库文件的external方法会让调用花费更多,但部署文件的gas和体积更小,因此是一个折中。
特别注意: 如果lib中有externel或者public方法,则lib一定需要独立部署,remix会自动转化, 而hardhat中要注意填入link信息,否则会报错。
上图交易的花费为: 115901
调用bar的花费为:49858 ,26973
上图两个交易分别花费: 122886,146252, 后续再部署Bar都是花费 146252。
调用bar方法的gas为53650, 30765
部署lib
let libFactory = await hre.ethers.getContractFactory("IdentityLib");
let lib = await libFactory.deploy();
await lib.deployed();
console.log("library deployed to address: ", lib.address);
部署合约,并在部署时link lib
let contractFactory = await hre.ethers.getContractFactory(
"CarbonIslandMethodology",
{
signer: deployer,
libraries: {
IdentityLib: lib.address,
},
}
);
let contract = await contractFactory.deploy();
await contract.deployed();
console.log("contract deployed to address: ", contract.address);
最小代理提供了一个最精简的代理合约代码。
使用最小代理的注意事项 :最小代理的实现合约地址不能改变,这意味着你将不能升级他们的代码。
https://eips.ethereum.org/EIPS/eip-1167
https://mirror.xyz/xyyme.eth/mmUAYWFLfcHGCEFg8903SweY3Sl-xIACZNDXOJ3twz8
log的gas使用参数如下
LogDataGas uint64 = 8 // Per byte in a LOG* operation's data.
LogGas uint64 = 375 // Per LOG* operation.
LogTopicGas uint64 = 375
MemoryGas uint64 = 3
其基础计算公式为
gas_cost = 375 + 375 * num_topics + 8 * data_size + mem_expansion_cost
一个例子:如果是2个topic + 200 bytes的log, 花费的gas为:
375 (static cost)
200 = 200 bytes of memory for log.Data x 3 cost of memory = 600 gas for memory gas
2 x 375 = 750 for topic gas
8 x 200 = 1600 for log.Data cost
Total cost: 375 + 600 + 750 + 1600 = 3,325 gas units
可见相比存储一个uint256, 需要20000+ gas ,要节省很多。
小结:
简而言之,默克尔证明使用单个数据块来证明大量数据的有效性。
无状态合约利用了交易数据和事件调用等内容完全保存在区块链上的事实。因此,你不需要不断地改变合约的状态,而只需发送一笔交易并传递您想要存储的值即可。由于 SSTORE 操作通常占大部分交易成本,因此无状态合约仅消耗有状态合约的一小部分 Gas。
contract DataStore {
function save(bytes32 key, string value) {}
}
然后可以使用ethereum-input-data-decoder` 来直接解析交易的inputdata
npm install ethereum-input-data-decoder
### 使用链下数据源
如ipfs等,但存在大量以及非结构化数据时, 适合用链下数据源方式。我们可以将数据广播到 IPFS 网络,然后将相应的哈希值保存在合约中,以便稍后引用该信息。
降低链上存储方法小结:
Index Logs | Stateless Contract | Merkle Proof | Off-chain(IPFS) | |
---|---|---|---|---|
数据规模 | 小 | 小 | 中小 | 大 |
gas节省度 | 小(log本身也需gas) | 中 | 大 | 大 |
数据能否被合约使用 | 不能 | 不能 | 可以。(附带merkle路径) | 比较麻烦,需要预言机等方式喂入数据使用 |
如何修改数据 | 链下认定新数据覆盖旧数据 | 链下认定新数据覆盖旧数据 | 链下修改,链上更新root | 较复杂,要自行实现修改方式 |
适用场景 | 不需要再合约中使用变量,上链的最主要行为就是记录,其他行为由链下完成。有多重行为需要分类提醒链下处理 | 不需要再合约中使用变量,上链的最主要行为就是记录, 其他行为由链下完成 | 不需要频繁地访问和更改、添加链上变量,主要是批量数据的记录和偶然的单个验证查询 | 链下数据量大,且存在如图片等非结构化数据。主要用于辅助合约存储一些不在合约中写入和计算的额外数据 |
https://mirror.xyz/quentangle.eth/GxmosHtVYZaIkJjM9slpkKtZWfk8fU78FQ8oxoDNuFE
gas优化
https://ethereum.stackexchange.com/questions/28813/how-to-write-an-optimized-gas-cost-smart-contract
https://medium.com/layerx/how-to-reduce-gas-cost-in-solidity-f2e5321e0395
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。