はじめに
Truffle 環境の設定をする truffle-config.js ですが、最適化ブロックで runs という指定が使えるようです。
はて、この値はなんなんでしょう?
Solidity の公式ページの説明からするに、solc においての runs は下記のようなものらしいです。
デフォルトでは、オプティマイザは、コントラクトがそのライフタイムにわたって200回呼び出されると仮定して、コントラクトを最適化します。 イニシャルコントラクトのデプロイのガス代をより安く、その後の関数実行時のガス代をより高くしたい場合は、--optimize-runs=1 と設定してください。 多くのトランザクションを想定していて、デフォルトより高いデプロイコストとアウトプットサイズを気にしないのであれば、 --optimize-runs を大きな数に設定してください。
どうやら runs の設定は、**「値を小さくするとデプロイ時のガス代が下がって トランザクションのガス代が上がる、値を大きくするとデプロイ時のガス代が上がって トランザクションのガス代が下がる」**とのこと。
Truffle 環境ではコンパイラとして solcjs が使われているらしいので solc の機能がそのまま使えるとは限らないのですが、物は試しです。どのような効果があるのかテストしてみましょう(※こちらがテストプロジェクトとなります)。
テスト方法
ガス消費の計測用に、構造体を1つ作成して動的配列に追加する関数を持つ、シンプルなコントラクトを準備します(※せっかくなので CryptoKitties の Kitty 構造体を拝借しました)。
テストコード(クリックで開閉します)
pragma solidity 0.5.12;
contract CompileOpt5{
// クリプトキティの構造体を拝借
struct Kitty {
uint256 genes;
uint64 birthTime;
uint64 cooldownEndBlock;
uint32 matronId;
uint32 sireId;
uint32 siringWithId;
uint16 cooldownIndex;
uint16 generation;
}
// ニャンコの配列
Kitty[] internal kitties;
// 要素数の取得
function getTotalKitties() public view returns( uint256 ){
return kitties.length;
}
// ニャンコの追加
function createKitty( uint256 _val256 ) public{
Kitty memory _kitty = Kitty({
genes: _val256,
birthTime: uint64(_val256),
cooldownEndBlock: uint64(_val256),
matronId: uint32(_val256),
sireId: uint32(_val256),
siringWithId: uint32(_val256),
cooldownIndex: uint16(_val256),
generation: uint16(_val256)
});
kitties.push( _kitty );
}
}
上記コードを、runs の値が [1]、[200]、[2000] の設定でコンパイルしたコントラクトを、それぞれデプロイした際のガス消費量と、関数にアクセスした際のガス消費量で比較します。ついでに、最適化を無効にした状態もテストしてみましょう。
Truffle v5.1.2 (solc-js v0.5.12) でのテスト
各コントラクトをデプロイし、createKitty 関数へ200回以上アクセスをしてみました(※ひょっとしたら runs で指定した回数のトランザクションを超えるとコストが高くなったりするのかと思ったので多めにテスト)。
で、結果としては、すべてのコントラクトにおいて初回トランザクションだけガス消費が低く、2回目以降は一律の消費という結果になりました。
・最適化 有り(runs=1)
デプロイ時のガス消費: 187,711
初回のガス消費: 45,551
2回目以降のガス消費: 68,963
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化有り:runs=1
compilers: {
solc: {
version: "0.5.12",
parser: "solcjs",
settings: {
optimizer: {
enabled: true,
runs: 1
}
}
}
}
}
・最適化 有り(runs=200)
デプロイ時のガス消費: 190,327
初回のガス消費: 45,503
2回目以降のガス消費: 68,915
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化有り:runs=200(デフォルト値)
compilers: {
solc: {
version: "0.5.12",
parser: "solcjs",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
}
}
・最適化 有り(runs=2000)
デプロイ時のガス消費:245,294
初回のガス消費: 45,419
2回目以降のガス消費: 68,831
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化有り:runs=2000
compilers: {
solc: {
version: "0.5.12",
parser: "solcjs",
settings: {
optimizer: {
enabled: true,
runs: 2000
}
}
}
}
}
・最適化 無し
デプロイ時のガス消費: 220,118
初回のガス消費: 55,710
2回目以降のガス消費: 79,122
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化無し
compilers: {
solc: {
version: "0.5.12",
parser: "solcjs",
settings: {
optimizer: {
enabled: false
}
}
}
}
}
さて、各値を比較してみるに、runs の値が小さいほどデプロイ時のガス消費が抑えられているようです。
一方で、runs の値による、トランザクションのコスト変動は微々たるものです。デフォルトの runs=200 に対して、トランザクションを犠牲するはずの runs=1 でも、 優遇されるはずの runs=2000 においても、恩恵を実感できるほどの差が出ませんでした。
また、最適化無しの状態にくらべると、2回目以降のトランザクションのガス消費が**13%**ほど改善されています。
まとめ
truffle-config.js において runs の値は、特に気にしなくても良さそうです。とりあえず最適化しておくだけで、ガス消費も多少収まってくれるみたいですし、DApp のリリース時に最適化し忘れさえしなければ問題はなさそうですね。
Truffle v4 での最適化はどうなるのか?
さて、ある意味ここからが本題かもしれません。
最適化の指定は Truffle v4 環境でもちゃんと効果があるのでしょうか?
Truffle 関連の情報、特に書籍等では、v4 系の内容を取り扱ったものが多い印象があります。また、truffle console の使い勝手が、v4 と v5 で結構変わっているので、あえて v4 環境を使い続けている方も少なくないと思います。折角なので、v4 系でもテストしてみましょう。
Truffle v4.1.15 (solcjs v0.4.25) でのテスト
では、v4 向けに名前とコンパイラのバージョンを調整したコントラクトを用意して、テストの開始です。
テストコード(クリックで開閉します)
pragma solidity 0.4.25;
contract CompileOpt4{
// クリプトキティの構造体を拝借
struct Kitty {
uint256 genes;
uint64 birthTime;
uint64 cooldownEndBlock;
uint32 matronId;
uint32 sireId;
uint32 siringWithId;
uint16 cooldownIndex;
uint16 generation;
}
// ニャンコの配列
Kitty[] internal kitties;
// 要素数の取得
function getTotalKitties() public view returns( uint256 ){
return kitties.length;
}
// ニャンコの追加
function createKitty( uint256 _val256 ) public{
Kitty memory _kitty = Kitty({
genes: _val256,
birthTime: uint64(_val256),
cooldownEndBlock: uint64(_val256),
matronId: uint32(_val256),
sireId: uint32(_val256),
siringWithId: uint32(_val256),
cooldownIndex: uint16(_val256),
generation: uint16(_val256)
});
kitties.push( _kitty );
}
}
・最適化 有り(runs=1)
デプロイ時のガス消費:225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化有り:runs=1
compilers: {
solc: {
version: "0.4.25",
parser: "solcjs",
settings: {
optimizer: {
enabled: true,
runs: 1
}
}
}
}
}
・最適化 有り(runs=200)
デプロイ時のガス消費:225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化有り:runs=200(デフォルト値)
compilers: {
solc: {
version: "0.4.25",
parser: "solcjs",
settings: {
optimizer: {
enabled: true,
runs: 200
}
}
}
}
}
・最適化 有り(runs=2000)
デプロイ時のガス消費: 225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化有り:runs=2000
compilers: {
solc: {
version: "0.4.25",
parser: "solcjs",
settings: {
optimizer: {
enabled: true,
runs: 2000
}
}
}
}
}
・最適化 無し
デプロイ時のガス消費:225,602
初回のガス消費: 55,701
2回目以降のガス消費: 79,113
truffle-config.js(クリックで開閉します)
module.exports = {
// 〜 省略 〜
// 最適化無し
compilers: {
solc: {
version: "0.4.25",
parser: "solcjs",
settings: {
optimizer: {
enabled: false
}
}
}
}
}
やや、すべての設定において、ガス消費量が完全に同じ値となりました…。
最初はコンパイル指定をミスったと思ったのですが、イーサスキャンでバイトコードを眺めてみると、各コントラクトともバイナリレベルで若干の差が出ています。仮に、同じバイナリデータであれば、イーサスキャンで表示した際、重複コントラクト扱いされるはずなので、個別に表示できている時点でコントラクト間で差分があるのは間違いなさそうです。
それなのに、イーサスキャン上でベリファイコードを登録しようとすると、「Optimization Enabled: No」でないと受け付けてくれません。
まとめると、最適化の設定によりバイナリデータに差が出ているにも関わらず、イーサスキャン上では最適化が認識されていないようなのです。
そして最大の問題が**「最適化有りでも無しでも、すべてのコントラクトのガス消費量が同じになっている」**という事実。
この結果が、私の環境や手順の間違いによるものであればよいのですが、仮に、 Truffle v4 環境が最適化の指定をコントラクトに反映してくれないのであれば、由々しき事態です。
もし Truffle v4 をお使いで Dapp のリリースを念頭に置いている方がいらっしゃるのであれば、お早めに、お使いの環境で最適化が機能するかテストしてみることをお勧めします。リリース間際になって「最適化が効かない!?」なんて状況は考えるだに恐ろしいです。