在以太坊上,代码即法律,交易即金钱。每一笔智能合约的运行,都要根据复杂度消耗一笔GAS费(ETH)。那么,智能合约solidity语言的编写,不只要考虑安全,也要考虑语言的优化,以便高效便宜了。html
本文将从如下一些方面分析如何节约GAS的编程总结:git
1)如何在REMIX编译器上分析GAS/GAS LIMIT等信息 2) 如何优化节省GAS费用的方法github
建立合约优化编程
存储优化数组
变量排序优化安全
交易输入数据优化微信
转帐优化网络
部署合约优化app
调用合约函数的成本优化编辑器
若是你想了解以太坊的帐户、交易、Gas和Gas Limit等基本概念信息,能够阅读文章《以太坊的帐户、交易、Gas和Gas Limit》。
若是你不了解以太坊智能合约语言solidity编译IDE环境REMIX,能够阅读文章《Solidity语言编辑器REMIX指导大全》。
本章节聚焦在如何经过REMIX编译器查看GAS/GAS LIMIT等信息。
2.1 简单智能合约样例
以太坊指令执行主要依靠GAS。当你执行智能合约时,它会消耗GAS。因此,若是你正在运行一个智能合约,那么每一条指令都要花费必定数量的GAS费。这有两个因素,即您发送的GAS数量和总区块GAS上限(a total block gas limit)。
举例来讲,一个简单的智能合约,有一个保存无符号整数256值的函数。 合约代码以下:
pragma solidity ^0.4.19; contract A { uint b; function saveB(uint _b) public { b = _b; } }
若是你将此合约复制并粘贴到Remix(http://remix.ethereum.org/)中,则能够运行此合约。经过MIST或来自网站的MetaMask与此合同进行交互的方式相似。
让咱们运行saveB(5)并查看日志窗口中发生的状况:
这儿有3个咱们感兴趣的值:
GAS总量( "gas limit"): 3,000,000
交易费用 ("transaction cost"): 41642 gas
执行费用( "execution cost"): 20178 gas.
2.2 发送的GAS总量(Gas limit)
这儿显示的"Gas limit"是发送的GAS总量,Value是发给目标地址的ETH值。这2处的值能够被发送交易的用户修改。
2.3 交易成本(Transaction Cost)
交易成本,在Remix中显示,是实际交易成本加上执行成本的混合。我认为,这儿看起来有点误导。
若是您使用数据字段发送交易,那么交易包含一个基本成本和每一个字节的附加成本(GAS计价)。看看以太坊黄皮书(https://github.com/riversyang/ethereum_yellowpaper)的附录列出了每种的GAS费用:
一块儿来看看41642的交易成本是如何结合在一块儿的。这是Remix在交易中自动发送的数据字段:
input_remix
这儿是 Data-Field:
> 0x348218ec0000000000000000000000000000000000000000000000000000000000000005
数据字段是散列函数签名的前4个字节和32字节填充参数的组合。咱们快速手动计算。
函数签名是saveB(uint256),若是咱们用SHA3-256(或Keccak-256)散列函数,那么咱们获得:348218ec5e13d72ab0b6b9db1556cba7b0b97f5626b126d748db81c97e97e43d
若是咱们取前4个字节(提醒:1个字节= 8位= 2个十六进制字符.1个十六进制字符= 4 bit = 0-15 = 0000到1111 = 0x0到0xF),而后咱们获得348218ec。让咱们0x在前面添加,咱们获得0x348218ec。参数是一个256位的无符号整数,即32个字节。这意味着它将整数“5”填充到32个字节,换句话说,它将在数字前面添加63个零: 0000000000000000000000000000000000000000000000000000000000000005。
从以太坊黄皮书上能够得到参考:
每笔交易都有21000 GAS支付
为交易的每一个非零字节数据或代码支付68 GAS
为交易的每一个零字节数据或代码支付4 GAS
计算一下: 348218ec 是4个字节的数据,显然是非零的。 0000000000000000000000000000000000000000000000000000000000000005是31个字节的零数据和1个字节的非零数据的混合。
这使得总共5个字节的非零数据和31个字节的零数据。
(5 non-zero-bytes * 68 gas) + (31 zero-bytes * 4 gas) = 340 + 124 = 464 gas
对于咱们的输入数据,咱们必须支付464 GAS。除此以外,咱们还要支付 21000 GAS,这是每笔交易支付的。所以总共须要21464用于交易。 让咱们看看是否会增长。
Remix称“交易成本”为41642 gas,“执行成本”为 20178 gas。而在Remix中,“交易成本”其实是交易成本加执行成本的总和。所以,若是咱们从交易成本中减去执行成本,咱们应该获得21464 gas。
41642 (交易成本”) - 20178 (执行成本) = 21464 gas
剩下的结果21464 gas为数据交易成本,同上计算公式。
2.4 执行成本(Execution Cost)
执行成本有点难以计算,由于发生了不少事情,辉哥试着告诉你合同执行时到底发生了什么。
让咱们深刻了解实际的事务并打开调试器。这能够经过单击事务旁边的“调试”按钮来完成。
能够打开指令折叠菜单和单步调试菜单。你将看到每一条指令以及每一个指令在该特定步骤中花费的GAS费用。
这里看到的是全部以太坊汇编指令。所以,咱们知道Solidity能够归结为EVM Assembly。这是矿工实际执行的智能合约运行看起来的实际状况。来看看前两个指令:
PUSH1 60 PUSH1 40
这意味着除了将值60和40推入堆栈以外别无其余。显然还有不少事情要作,你能够经过在单步调试器中移动蓝色滑块来完成它们的工做。
根据以太坊黄皮书将每一个指令所需的确切气体量汇总在一块儿,以便将值5写入存储:
GAS Instruction 3 000 PUSH1 60 3 002 PUSH1 40 12 004 MSTORE 3 005 PUSH1 04 2 007 CALLDATASIZE 3 008 LT 3 009 PUSH1 3f 10 011 JUMPI 3 012 PUSH1 00 3 014 CALLDATALOAD 3 015 PUSH29 0100000000000000000000000000000000000000000000000000000000 3 045 SWAP1 5 046 DIV 3 047 PUSH4 ffffffff 3 052 AND 3 053 DUP1 3 054 PUSH4 348218ec 3 059 EQ 3 060 PUSH1 44 10 062 JUMPI 1 068 JUMPDEST 2 069 CALLVALUE 3 070 ISZERO 3 071 PUSH1 4e 10 073 JUMPI 3 074 PUSH1 00 3 076 DUP1 1 078 JUMPDEST 3 079 PUSH1 62 3 081 PUSH1 04 3 083 DUP1 3 084 DUP1 3 085 CALLDATALOAD 3 086 SWAP1 3 087 PUSH1 20 3 089 ADD 3 090 SWAP1 3 091 SWAP2 3 092 SWAP1 2 093 POP 2 094 POP 3 095 PUSH1 64 8 097 JUMP 1 100 JUMPDEST 3 101 DUP1 3 102 PUSH1 00 3 104 DUP2 3 105 SWAP1 20000 106 SSTORE 2 107 POP 2 108 POP 8 109 JUMP 1 098 JUMPDEST 0 099 STOP
合计为20178 GAS费。
2.5 GAS上限(Gas Limit)
因此,以太坊区块链上的每一条指令都会消耗一些GAS。若是你要将值写入存储,则须要花费不少。若是你只是使用堆栈,它的成本会低一些。但基本上全部关于EVM的指令都须要GAS。这意味着智能合约只能作有限的事情,直到发送的GAS用完为止。在样例这种状况下,咱们发送了300万 GAS费。
当您返回REMIX的单步调试器,点击第一步时,您会看到每一个步骤剩余多少GAS。辉哥在第一步打开它:
它已经从咱们发送的300万(从3,000,000 - 21464 = 2,978,536)中扣除的交易成本开始。(说明:21464是以前2.3章节执行的数据执行成本。)
一旦此计数器达到零,那么合约执行将当即中止,全部存储的值将被回滚,你将得到“Out of Gas”异常告警。
2.6 区块GAS上限(Block Gas Limit)
除了经过交易设置的气Gas Limit外,还有一个所谓的“区块上限”。这是你能够发送的最大GAS量。目前,在Main-Net,该值大概为8M左右。
2.7 GAS退款(Gas Refund)
Gas Limit有一个好处:你没必要本身计算它。若是你向合约发送8M的GAS,它耗尽41642 GAS,能够退还其他部分。所以,发送远远超过必要的GAS总会节省下来的,其他的将自动退还到你的帐号地址。
2.8 GAS价格(Gas Price)
GAS价格决定了交易在可否被包含在下一个被挖出的区块中。
当你发送交易时,你能够激励矿工接下来处理您的交易。这种激励就是GAS PRICE。矿工一旦挖出新区块,也会将交易归入该区块。哪些交易被归入下一个区块是由矿工肯定的 - 但他极可能将GAS PRICE从高到低排序。
假设有15笔未完成的交易,但只有12笔交易能够进入下一个区块。5个20 Gwei,5个15 Gwei和5个 5Gwei的GAS PRICE。矿工极可能按此顺序选择交易:5 * 20 + 5 * 15 + 2 * 5 Gwei并将它们合并到下一个挖掘区块中。
所以,GAS Limit基本上决定了以太坊虚拟机能够执行的指令数量,而GAS Price决定了矿工选择此交易的可能性。
大多数钱包将标准GAS Price设定为20Gwei左右(0.00000002 ETH)。若是您正在执行上述合约,那么您将支付约60-70美分(美圆分),当前汇率为1 ETH = 800美圆。因此它根本不便宜。
幸运的是,在网络拥塞期间,您只须要更高的GAS PRICE,那是由于许多人尝试同时发送交易。若是网络没有拥挤,那么您不须要支付这么多GAS。EthGasStation网站(https://ethgasstation.info)评估目前的交易价格为4 Gwei足够 。因此,凭借这个小功能,只须要4 Gwei的GAS,它将是16美分左右,而不是65美分。一个巨大的差别。
GAS消耗可参考如下两个表:
表格1
表2
下面提供一下优化GAS消耗的方法。
3.1 建立合约
建立合约对应CREATE和CODECOPY这两条指令。在合约中建立另外一个空合约消耗42,901个GAS(总共64,173个GAS)。若是直接部署空白合约,共有68,653个GAS。
若是包含实施,可能会有数十万甚至数百万的GAS。它应该是全部指令中最昂贵的。若是建立多个合约实例,则GAS消耗可能很大。
建议: 避免将合约用做数据存储。
很差的代码实现:
contract User { uint256 public amount; bool public isAdmin; function User(uint256 _amount, bool _isAdmin) { amount = _amount; isAdmin = _isAdmin; } }
好的代码实现:
contract MyContract { mapping(address => uint256) amount; mapping(address => bool) isAdmin; }
另外一种OK的代码实现:
contract MyContract { struct { uint256 amount; bool isAdmin; } mapping(address => User) users; }
3.2 存储
对应于SSTORE指令。存储新数据须要20,000 GAS。修改数据须要5000 GAS。一个例外是将非零变量更改成零。咱们稍后会讨论这个问题。
建议: 避免重复写入,最好一次在最后尽量多地写入到存储变量。
很差的代码样例:
uint256 public count; // ... for (uint256 i = 0; i < 10; ++i) { // ... ++count; }
好的代码样例:
for (uint256 i = 0; i < 10; ++i) { // ... } count += 10;
3.3 变量排序对GAS的影响
你可能不知道变量声明的顺序也会影响Gas的消耗。
因为EVM操做都是以32字节为单位执行的,所以编译器将尝试将变量打包成32字节集进行访问,以减小访问时间。
可是,编译器不够智能,没法自动优化变量分组。它将静态大小的变量分组为32个字节的组。例如:
contract MyContract { uint64 public a; uint64 public b; uint64 public c; uint64 public d; function test() { a = 1; b = 2; c = 3; d = 4; } }
执行test()时,看起来已经存储了四个变量。因为这四个变量之和刚好是32个字节,所以实际执行了一个SSTORE。这只须要20,000 GAS。
再看下一个例子:
contract MyContract { uint64 public a; uint64 public b; byte e; uint64 public c; uint64 public d; function test() { a = 1; b = 2; c = 3; d = 4; } }
中间插入了另外一个变数,结果形成a,b,e和c会被分为一组,d独立为一组。一样的test()形成两次写入,消耗40000 Gas。
最后再看一个例子:
contract MyContract { uint64 public a; uint64 public b; uint64 public c; uint64 public d; function test() { a = 1; b = 2; // ... do something c = 3; d = 4; } }
**这与第一个例子的区别在于:**在存储a和b以后,完成了其余事情,最后存储了c和d。结果此次将致使两次写入。由于当执行“执行某事”时,编译器肯定打包操做已结束,而后发送写入。可是,因为第二次写入是同一组数据,所以认为它是被修改的。将消耗总共25,000个气体。
建议:
根据上述原则,咱们能够很容易地知道如何处理它。
很差的代码例子:
contract MyContract { uint128 public hp; uint128 public maxHp; uint32 level; uint128 public mp; uint128 public maxMp; }
好的例子:
contract MyContract { uint128 public hp; uint128 public mp; uint128 public maxHp; uint128 public maxMp; uint32 level; }
这里咱们假设hp和mp更频繁地更新,而且maxHp和maxMp更频繁地一块儿更新。
很差的代码例子:
function test() { hp = 1; // ... do something mp = 2; } 好的例子: function test() { // ... do something hp = 1; mp = 2; }
这个规则在struct上是同样的。
3.4 交易输入数据
合约交易的基本气体是21,000。输入数据为每字节68个GAS,若是字节为0x00则为4个GAS。
例如,若是数据为0x0dbe671f,则气体为68 * 4 = 272; 若是是0x0000001f,它是68 * 1 + 4 * 3 = 80。
因为全部参数都是32字节,所以当参数为零时,气体消耗最小。它将是32 * 4 = 128。最大值以下:
n * 68 +(32-n)* 4 的字节数 (n:参数)
例如,32字节输入参数的最大GAS为2,176 (3268 = 2176)。输入参数为地址,地址是20个字节,所以它是1,408 (2068+(32-20)*4 = 1408)。
建议: 能够经过更改排序来节省GAS消耗。
例如EtherScan有下一段交易记录:
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
全部参数都是256位,不管类型是byte32,address仍是uint8。因此左边的大多数参数都有大量的“0”是未使用的位。很容易想到使用这些“空间”。
例如能够把tokenGive的高位字节用于存放下面吗一些变量,把命名改成uint256 tokenSellWithData。
nonce - > 40位 takerFee - > 16位 makerFee - > 16位 uint256 joyPrice - > 28位 isBuy - > 4位(实际上,1位就足够了。只是为了方便呈现文档)
假如上面变量的值分别为:
nonce: 0181bfeb takerFee: 0014 makerFee: 000a joyPrice: 0000000 isBuy: 1
那么tokenSellWithData的存储可能如:
更多优化参考文章《[Solidity] Compress input in smart contract》。
3.5 转帐
Call, send 和transfer 函数对应于CALL指令。基本消耗是7,400 GAS。事实上,消费将近7,600 GAS。值得注意的是,若是转帐到一个从未见过的地址,将额外增长25,000个GAS。
没有额外的消耗样例:
function withdraw(uint256 amount){ msg.sender.transfer(amount); }
可能会有额外的消耗样例(receiver参数未被使用,多余参数):
function withdrawTo(uint256 amount, address receiver) { receiver.transfer(amount); }
3.6 其余命令
3.6.1 ecrecover
对应CALL指令。此功能将消耗3700 GAS。
3.6.2调用外部合约
调用外部合约执行EXTCODESIZE和CALL指令。基本消耗1400 GAS。除非必要,不然不建议拆分多个合同。可使用多个继承来管理代码。
3.6.3事件
对应于LOG1指令。没有参数的事件是750 GAS。理论上每一个附加参数将增长256个GAS,但事实上,它会更多。
3.6.4哈希
你可使用智能合约中的几个内置哈希函数:keccak256,sha256和ripemd160。参数越多,消耗的气体越多。耗气量:ripemd160> sha256> keccak256。所以,若是没有其余目的,建议使用keccak256函数。
3.7 部署合约优化
大部分的优化在编译时候已经完成了。
问题:部署合同中是否包含注释,是否会增长部署气体? 回答:不,在编译期间删除了执行时不须要的全部内容。其中包括注释,变量名和类型名称。
而且能够在此处文章(https://solidity.readthedocs.io/en/latest/miscellaneous.html#internals-the-optimizer)找到优化程序的详细信息。
另外一种经过删除无用代码来减少大小的方法。例如:
1 function p1 ( uint x ){ 2 if ( x > 5) 3 if ( x*x < 20) 4 XXX }
在上面的代码中,第3行和第4行永远不会执行,而且能够避免这些类型的无用代码仔细经过合同逻辑,这将减小智能合约的大小。
3.8 调用合约函数的成本优化
当调用合约额的功能时,为了执行功能,它须要GAS。所以,优化使用较少GAS的功能很是重要。在考虑每一个合约时时,能够采用多种不一样的方式。这里有一些可能在执行过程当中节省GAS的方法。
3.8.1 减小昂贵的操做
昂贵的操做是指一些须要更多GAS值的操做码,例如SSTORE。如下是一些减小昂贵操做的方法。
A)使用短路规则(https://solidity.readthedocs.io/en/develop/types.html#booleans)
操做符 || 和&&适用常见的短路规则。这意味着在表达式f(x)|| g(y)中,若是f(x)的计算结果为真,即便它有反作用,也不会评估g(y)。
所以,若是逻辑操做包括昂贵的操做和低成本操做,那么以昂贵的操做能够短路的方式安排将在一些执行中减小GAS。
若是f(x)是便宜的而且g(y)是昂贵的,逻辑运算代码(便宜的放在前面):
OR : f(x) || g(y)
AND: f(x) && g(y)
若是短路,将节省更多的气体。
f(x)与g(y)安排AND操做相比,若是返回错误的几率要高得多,f(x) && g(y)可能会致使经过短路节省更多的气体。
f(x)与g(y)安排OR运算相比,若是返回真值的几率要高得多,f(x) || g(y)可能会致使经过短路节省更多气体。
B)循环中昂贵的操做
很差的代码,例如:
uint sum = 0; function p3 ( uint x ){ for ( uint i = 0 ; i < x ; i++) sum += i; }
在上面的代码中,因为sum每次在循环内读取和写入存储变量,因此在每次迭代时都会发生昂贵的存储操做。这能够经过引入以下的局部变量来节省GAS来避免。
好的代码,例如:
uint sum = 0; function p3 ( uint x ){ uint temp = 0; for ( uint i = 0 ; i < x ; i++) temp += i; } sum += temp;
3.8.2 其余循环相关模式
循环组合,很差的代码样例:
function p5 ( uint x ){ uint m = 0; uint v = 0; for ( uint i = 0 ; i < x ; i++) //loop-1 m += i; for ( uint j = 0 ; j < x ; j++) /loop-2 v -= j; }
loop-1和loop-2能够组合,能够节省燃气。
好的代码样例:
function p5 ( uint x ){ uint m = 0; uint v = 0; for ( uint i = 0 ; i < x ; i++) //loop-1 m += i; v -= j; }
3.8.3 使用固定大小的字节数组
可使用一个字节数组做为byte [],但它在传入调用时浪费了大量空间,每一个元素31个字节。最好使用bytes。
根据经验,对任意长度的原始字节数据使用 bytes标识符,对任意长度的字符串(UTF-8)数据使用 string标识符。若是您能够将长度限制为特定的字节数,请始终使用bytes1到bytes32之一,由于它们要便宜得多。
具备固定长度老是节省GAS。也请参考这个问题(https://ethereum.stackexchange.com/questions/11556/use-string-type-or-bytes32)描述。
3.8.4 删除无用的代码能够在执行时节省GAS
如前面在合同部署中所解释的那样删除无用的代码即便在执行函数时也会节省GAS。
3.8.5 在实现功能时不使用库对于简单的使用来讲更便宜。
调用库以得到简单的用法可能代价高昂。若是功能在合同中实现简单且可行,由于它避免了调用库的步骤。两种功能的执行成本仍然相同。
参考
(1)区块链系列十九:Gas优化:https://magicly.me/blockchain-19-solidity-gas-optimization
(2)How to write an optimized (gas-cost) smart contract?:https://ethereum.stackexchange.com/questions/28813/how-to-write-an-optimized-gas-cost-smart-contract
(3)[Solidity] Optimize Smart Contract Gas Usage:https://medium.com/joyso/solidity-save-gas-in-smart-contract-3d9f20626ea4
(4)What exactly is the Gas Limit and the Gas Price in Ethereum:https://vomtom.at/what-exactly-is-the-gas-limit-and-the-gas-price-in-ethereum/
本文做者:HiBlock区块链技术布道群-辉哥
原文发布于简书
加微信baobaotalk_com,加入技术布道群