アルゴリズムとかオーダーとか

仕事で勉強したことなどをまとめてます

32byte blockを意識してgas代を節約しよう

Ethereumではstorageにデータを格納する場合、32byteごとにgas代がかかります。この32byteの境界を意識することでガス代が節約できます。
という話をたまに聞くのですが、実体験として経験したことがなかったので実際に試してみました。
今回は試してみたテストコードと節約できたgas代を提示し、なぜgas代が節約できるのかを解説します。

サンプルコントラクト

今回は以下のコントラクトでテストしました。
setNumではuint256の値を2つ書き込みます。なので32byte * 2で2つのストレージブロックを使います。
setShortではuint128の値を2つ書き込むので、16byte * 2 でストレージブロック1つだけを消費し(てくれる予定)ます。

pragma solidity ^0.4.19;

contract StrageOptimizing {
  uint public number1;
  uint public number2;

  uint128 public short1;
  uint128 public short2;

  function setNum(uint _num1, uint _num2) public {
    number1 = _num1;
    number2 = _num2;
  }

  function setShort(uint128 _s1, uint128 _s2) public {
    short1 = _s1;
    short2 = _s2;
  }
}

テストコード

truffleで試してたのでtruffleで動くテストコードを提示しています。

const StrageOptimizing = artifacts.require('StrageOptimizing.sol')
contract('StrageOptimizing', function(accounts) {
  it("should assert true", async () => {
    const ins = await StrageOptimizing.new();
    assert.isOk(ins);
  })
  it("cheep gas test when first", async () => {
    const ins = await StrageOptimizing.new();
    const normalGas = await ins.setNum.estimateGas(1, 2)
    const cheepGas = await ins.setShort.estimateGas(1, 2)

    console.log(cheepGas, normalGas)
    assert.isBelow(cheepGas, normalGas)
  })
  it("cheep gas test when modify", async () => {
    const ins = await StrageOptimizing.new();

    await ins.setNum(1, 2)
    await ins.setShort(1, 2)
    // check gas when modify
    const normalGas2 = await ins.setNum.estimateGas(1, 2)
    const cheepGas2 = await ins.setShort.estimateGas(1, 2)

    console.log(cheepGas2, normalGas2)
    assert.isBelow(cheepGas2, normalGas2)
  })
})

solidity compilerのoptimizeを有効化する

gas代の節約といえばoptimizerが重要になります。optimizerを有効にしていない場合は、初回の書き込みは安くなるのですが、2回目以降(値を変更するとき)のgas代がsetShortの方が若干高くなります。
truffle.jsを以下のように設定することでoptimizerを有効化します。

module.exports = {
  // See <http://truffleframework.com/docs/advanced/configuration>
  // to customize your Truffle configuration!
  solc: {
    optimizer: {
      enabled: true
    }
  }
};

実行結果

$ truffle test
Using network 'test'.



  Contract: StrageOptimizing
42107 61852
    ✓ cheep gas test when first (132ms)
27107 31852
    ✓ cheep gas test when modify (283ms)


  2 passing (455ms)

解説

gas代の違いについて解説します。

初回書き込み時

0から0以外の値に書き込みする場合は、storage利用料が20000gasかかります。つまり、setNumとsetShortで以下の違いが発生します。
※ちなみに、トランザクション発行手数料としてプラス21000gasされます。

  • setNum: 2回ストレージに書き込む。 20000 * 2 + 21000 = 約61000gas
  • setShort: 16byteのデータ2つなので、Solidityがよろしくビットシフトして32byteの中に詰め込んでくれる。そのためストレージへの書き込みが1回で済む。 20000 + 21000 = 約41000gas

2回目の書き込み時

0以外から0以外のデータをストレージに書き込む場合は5000のgas代がかかります。

  • setNum: 初回書き込み時と同様2回書き込む。 5000 * 2 + 21000 = 約31000gas
  • setShort: 同様によろしくビットシフトして32byteの中に2つのデータを詰め込んでから1回だけ書き込む。 5000 + 21000 = 約26000gas

optimizerを有効化しない場合

setShortのgas代だけが高くなります。どれくらいかというと+5000gasです。理由はコードに記述している通り素直に2回storageに値を書き込みに行くためです。optimizerが無効でもビットシフトをして32byteデータにまとめてくれるため、初回書き込み時も2回目の書き込み時も1つのストレージブロックに2回書き込みを行います。

まとめ

最初はSolidityのドキュメントに書いてあるように、structの場合だけ、32byteブロックを意識してcompile時にsolidityがよろしくビットシフトしてくれるのかなぁって思ってたのですが、実際はstruct関係なく積極的に32byteの中に押し込んでくれるということがわかりました。
頻繁に使えるテクニックではないですが、structを定義するときに少し意識してみると無駄なgas消費を抑えられるかもしれません。