Solidity 怎样写出最节省Gas的智能合约[译

如何节省gas 使用,是很多智能合约开发者头大的问题,这边文章应该会对大家有帮助。

在以太坊区块链上,Gas 是用来奖励矿工为智能合约的存储与执行所提供的算力。 目前以太坊的利用率逐渐增长,交易手续费成本也水涨传告 —— 现在每天的 gas 成本已经高达数百万美元。 随着以太坊生态系统的扩大,Solidity 智能合约开发者也需要关注 gas 使用的优化问题了。本文将介绍在使用 Solidity 开发以太坊智能合约时常用的一些 Gas 优化实践。

1、使用短路模式排序 Solidity 操作

短路(short-circuiting)是一种使用或/与逻辑来排序不同成本操作的 solidity 合约 开发模式,它将低 gas 成本的操作放在前面,高 gas 成本的操作放在后面,这样如果前面的低成本操作可行,就可以跳过(短路)后面的高成本以太坊虚拟机操作了。

// f(x) 是低gas成本的操作
// g(y) 是高gas成本的操作

// 按如下排序不同gas成本的操作
f(x) || g(y)
f(x) && g(y)

2、删减不必要的 Solidity 库

在开发 Solidity 智能合约时,我们引入的库通常只需要用到其中的部分功能,这意味着其中可能会包含大量对于你的智能合约而言其实是冗余的 solidity 代码。如果可以在你自己的合约里安全有效地实现所依赖的库功能,那么就能够达到优化 solidity 合约的 gas 利用的目的。

例如,在下面的 solidity 代码中,我们的以太坊合约只是用到了 SafeMath 库的add方法:

import './SafeMath.sol' as SafeMath;

contract SafeAddition {
 function safeAdd(uint a, uint b) public pure returns(uint) {
 return SafeMath.add(a, b);
 }
}

通过参考 SafeMath 的这部分代码的实现,可以把对这个 solidity 库的依赖剔除掉:

contract SafeAddition {
 function safeAdd(uint a, uint b) public pure returns(uint) {
 uint c = a + b;
 require(c >= a, "Addition overflow");
 return c;
 }
}

3、精确声明 Solidity 合约函数的可见性

在 Solidity 合约开发中,显式声明函数的可见性不仅可以提高智能合约的安全性, 同时也有利于优化合约执行的 gas 成本。例如,通过显式地标记函数为外部函数(External),可以强制将函数参数的存储位置设置为calldata,这会节约每次函数执行时所需的以太坊 gas 成本。

External 可见性比 public 消耗 gas 少。

4、使用适合的数据类型

在 Solidity 中,有些数据类型要比另外一些数据类型的 gas 成本高。有必要 了解可用数据类型的 gas 利用情况,以便根据你的需求选择效率最高的那种。 下面是关于 solidity 数据类型 gas 消耗情况的一些规则:

  • 在任何可以使用uint类型的情况下,不要使用string类型
  • 存储 uint256 要比存储 uint8 的 gas 成本低,为什么?点击这里查看原文
  • 当可以使用bytes类型时,不要在 solidity 合约种使用byte[]类型
  • 如果bytes的长度有可以预计的上限,那么尽可能改用 bytes1~bytes32 这些具有固定长度的 solidity 类型
  • bytes32 所需的 gas 成本要低于 string 类型

5、避免 Solidity 智能合约中的死代码

死代码(Dead code)是指那些永远也不会执行的 Solidity 代码,例如那些执行条件永远也不可能满足的代码,就像下面的两个自相矛盾的条件判断里的 Solidity 代码块,消耗了以太坊 gas 资源但没有任何作用:

function deadCode(uint x) public pure {
 if(x < 1 {
    if(x > 2) {
        return x;
    }
 }
}

6、避免使用不必要的条件判断

有些条件断言的结果不需要 Solidity 代码的执行就可以知道结果,那么这样的条件判断就可以精简掉。例如下面的 Solidity 合约代码中的两级判断条件,内层的判断是在浪费宝贵的以太坊 gas 资源:

function opaquePredicate(uint x) public pure {
 if(x < 1) {
    if(x < 0 ) {  // uint 不可能小于0
    return x;
    }
 }
}

7、避免在循环中执行 gas 成本高的操作

由于SLOADSSTORE操作码的成本高昂,因此管理 storage 变量的 gas 成本 要远远高于内存变量,所以要避免在循环中操作 storage 变量。例如下面的 solidity 代码中,num变量是一个 storage 变量,那么未知循环次数的若干次操作,很可能会造成 solidity 开发者意料之外的以太坊 gas 消耗黑洞:

uint num = 0;

function expensiveLoop(uint x) public {
  for(uint i = 0; i < x; i++) {
    num += 1;
  }
}

解决上述以太坊合约代码问题的方法,是创建一个 solidity 临时变量 来代替上述全局变量参与循环,然后在循环结束后重新将临时变量的值赋给全局状态变量:

uint num = 0;

function lessExpensiveLoop(uint x) public {
  uint temp = num;
  for(uint i = 0; i < x; i++) {
    temp += 1;
  }
  num = temp;
}

8、避免使用常量结果的循环

如果一个循环计算的结果是无需编译执行 Solidity 代码就可以预测的,那么 就不要使用循环,这可以可观地节省 gas。例如下面的以太坊合约代码就可以 直接设置 num 变量的值:

function constantOutcome() public pure returns(uint) {
  uint num = 0;
  for(uint i = 0; i < 100; i++) {
    num += 1;
  }
  return num;
}

9、合并循环

有时候在 Solidity 智能合约中,你会发现两个循环的判断条件一致,那么在这种情况下就没有理由不合并它们。例如下面的以太坊合约代码:

function loopFusion(uint x, uint y) public pure returns(uint) {
  for(uint i = 0; i < 100; i++) {
    x += 1;
  }
  for(uint i = 0; i < 100; i++) {
    y += 1;
  }
  return x + y;
}

10、避免循环中的重复计算

如果循环中的某个 Solidity 表达式在每次迭代都产生同样的结果,那么就可以将其 移出循环先行计算,从而节省掉循环中额外的 gas 成本。如果表达式中使用的变量是 storage 变量, 这就更重要了。例如下面的智能合约代码中表达式a*b的值,并不需要每次迭代重新计算:

uint a = 4;
uint b = 5;
function repeatedComputations(uint x) public returns(uint) {
  uint sum = 0;
  for(uint i = 0; i <= x; i++) {
    sum = sum + a * b;
  }
}

11. 去除循环中的比较运算

如果在循环的每个迭代中执行比较运算,但每次的比较结果都相同,则应将其从循环中删除。

function unilateralOutcome(uint x) public returns(uint) {
  uint sum = 0;
  for(uint i = 0; i <= 100; i++) {
    if(x > 1) {
      sum += 1;
    }
  }
  return sum;
} 

参考:

https://medium.com/coinmonks/optimizing-your-solidity-contracts-gas-usage-9d65334db6c7 https://ethereum.stackexchange.com/questions/28813/how-to-write-an-optimized-gas-cost-smart-contract https://arxiv.org/pdf/1703.03994.pdf https://ethereum.stackexchange.com/questions/3067/why-does-uint8-cost-more-gas-than-uint256 https://www.youtube.com/watch?v=qwBkeJ84d2g&feature=youtu.be&t=68 https://ethereum.stackexchange.com/questions/11556/use-string-type-or-bytes32

原文链接:How to Write Smart Contracts That Optimize Gas Spent on Ethereum