はじめに
いきなりですが、 CryptoKitties の Kitty 構造体を見てみましょう。
// The main Kitty struct. Every cat in CryptoKitties is represented by a copy of this structure,
// so great care was taken to ensure that it fits neatly into exactly two 256-bit words.
// Note that the order of the members in this structure is important because of the byte-packing
// rules used by Ethereum.
// Ref: http://solidity.readthedocs.io/en/develop/miscellaneous.html
struct Kitty {
uint256 genes;
uint64 birthTime;
uint64 cooldownEndBlock;
uint32 matronId;
uint32 sireId;
uint32 siringWithId;
uint16 cooldownIndex;
uint16 generation;
}
全ての変数の合計サイズが、きれいに 512 bit に収まっています。もちろんパディングも 0 bit。これは偶然ではなく、細心の注意を払った結果であることがコメントからも見てとれます。
クリプトキティーズをはじめとして、トークンの実装時など、Solidity で構造体を取り扱う機会は少なくありません。そして、その際に理解しておきたいのが構造体のパディングに関してです。
構造体のパディング
パディングとは、複数の変数を1つにまとめて取り扱う際、メンバーとなる変数がワード境界をまたがないようにするために、メモリ上に挿入される余白のことです。
一方で、この余白による落とし穴が、構造体のぱっと見のサイズと、メモリ上におけるサイズとの乖離が引き起こす「なんでこんなにストレージ喰ってるの?」問題です。
特に Solidity の場合、ワード長が 256 bit と大きいので、パデイングの影響がバカになりません。
では実際のところどうなのか? ためしにテストしてみました。
テストについて
テストに際して、**「EVMにおいて、未初期化の storage 変数はコントラクトの slot[0] を参照する」**という、ちょっと面白いクセを利用します。
具体的には、以下のような構造体を準備し、
struct Word8 {
uint256 word0;
uint256 word1;
uint256 word2;
uint256 word3;
uint256 word4;
uint256 word5;
uint256 word6;
uint256 word7;
}
下記のコードにて、コントラクトのストレージ先頭領域を、word8 変数経由で参照して、構造体のパディングの様子を眺めようというものです。
// 未初期化の word8 変数は slot[0](structForCheck) を参照する
Word8 storage word8;
テスト1:何気ない並びにおけるパディング
まずは、変数を型順に、何気なく並べた構造体でテストしてみましょう。
struct MyStruct {
address addr160;
uint256 val256;
uint160 val160;
uint64 val64;
uint32 val32;
uint16 val16;
uint8 val8;
}
テストコード上、この構造体のインスタンスをコントラクトの一番最初のメンバーとして配置し、チェックがしやすいようにコンストラクタにて構造体の各要素にダミー値を設定しておきます。
// structForCheck は word8 から参照される
MyStruct internal structForCheck;
// コンストラクタ
constructor() public {
// アドレスなので 0xAD の繰り返し
structForCheck.addr160 = address( 0xadADADadAdADAdadADADADadadADAdAdadaDAdAD );
// uint256 なので 0x256 の繰り返し
structForCheck.val256 = uint256( 0x2562562562562562562562562562562562562562562562562562562562562562 );
// uint160 なので 0x160 の繰り返し
structForCheck.val160 = uint160( 0x1601601601601601601601601601601601601601 );
// uint64 なので 0x64 の繰り返し
structForCheck.val64 = uint64( 0x6464646464646464 );
// uint32 なので 0x32 の繰り返し
structForCheck.val32 = uint32( 0x32323232 );
// uint16 なので 0x16 の繰り返し
structForCheck.val16 = uint16( 0x1616 );
// uint8 なので 0x8 の繰り返し
structForCheck.val8 = uint8( 0x88 );
}
これで、上述した word8 変数により、structForCheck の内容が参照できるようになりました(※ソースコードはこちら)。
(※実際にテストしてみたい方は、こちらのプロジェクトをご利用ください)
(※iOS アプリ上でのイーサリアムへの接続に関してはこちらの記事を参照ください)
テスト結果は下記となります。
@---------------------
@ checkStructBasic
@---------------------
w[0]: 000000000000000000000000adadadadadadadadadadadadadadadadadadadad
w[1]: 2562562562562562562562562562562562562562562562562562562562562562
w[2]: 3232323264646464646464641601601601601601601601601601601601601601
w[3]: 0000000000000000000000000000000000000000000000000000000000881616
w[4]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[5]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[6]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[7]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[0〜3] の4ワードが構造体の領域=メモリ上のサイズであることが見て取れます*(※[ff]の領域は structForCheck 変数の下に配置した番兵の値となります)*。
[00] の値の部分がパディング領域となり、w[0] の領域に 96 bit、w[3] の領域に 232 bit の余白が追加されていることがわかります。
構造体にコメントすると下記のようなメモリ配置となります。
// テスト1のパディング構成
struct MyStruct {
// w[0]:1ワード目
address addr160;
// 余白 96 bits
// w[1]:2ワード目
uint256 val256;
// w[2]:3ワード目
uint160 val160;
uint64 val64;
uint32 val32;
// w[3]:4ワード目
uint16 val16;
uint8 val8;
// 余白 232 bits
}
テスト2:最悪の並びにおけるパディング
つづいて、変数の並びをバイトサイズ順に調整した後、気まぐれで uint8 を1番上に移動した構造体と、そのテスト結果を見てみましょう(※ソースコードはこちら)。
struct MyStruct {
uint8 val8; // 気まぐれに一番上に持ってきた
uint256 val256;
address addr160; // サイズ順に並び替え
uint160 val160;
uint64 val64;
uint32 val32;
uint16 val16;
}
@---------------------
@ checkStructWorst
@---------------------
w[0]: 0000000000000000000000000000000000000000000000000000000000000088
w[1]: 2562562562562562562562562562562562562562562562562562562562562562
w[2]: 000000000000000000000000adadadadadadadadadadadadadadadadadadadad
w[3]: 3232323264646464646464641601601601601601601601601601601601601601
w[4]: 0000000000000000000000000000000000000000000000000000000000001616
w[5]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[6]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[7]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
構造体のサイズが1つ増えて5ワードになってしまいました。
w[0] の領域に 248 bit、w[2] の領域に 96 bit、w[4] の領域に 240 bit の余白ができてしまっています。
構造体にコメントすると下記のようなメモリ配置となります。
// テスト2のパディング構成
struct MyStruct {
// w[0]:1ワード目
uint8 val8;
// 余白 248 bits
// w[1]:2ワード目
uint256 val256;
// w[2]:3ワード目
address addr160;
// 余白 96 bits
// w[3]:4ワード目
uint160 val160;
uint64 val64;
uint32 val32;
// w[4]:5ワード目
uint16 val16;
// 余白 240 bits
}
テスト3:ベストの並びにおけるパディング
ここまでのテストでお気づきと思いますが、パディングは空き領域に対して収まりの悪い変数が出現した際に発生します。逆に言うと、できるだけ空き領域を活用するように変数を並べることで、パディングを最小限にすることができます。
テスト1のケースを改めて見てみましょう。
w[3] の領域に address addr160 (160 bit) が収まるだけの余白があります。なので、addr160 を uint16 val16 と uint8 val8 と同じ領域になるよう調整することで1ワード分改善できそうです。
(※別の見方として、 w[0] に uint16 val16 と uint8 val8 の収まる余白があると考えて、val16 と val8 を構造体の上部へ移動するのでも、もちろんOKです)
実際に調整した構造体とテスト結果が下記となります(※ソースコードはこちら)。
struct MyStruct {
uint256 val256;
uint160 val160;
uint64 val64;
uint32 val32;
address addr160; // w[3]の余白を活用するために移動
uint16 val16;
uint8 val8;
}
@---------------------
@ checkStructFit
@---------------------
w[0]: 2562562562562562562562562562562562562562562562562562562562562562
w[1]: 3232323264646464646464641601601601601601601601601601601601601601
w[2]: 000000000000000000881616adadadadadadadadadadadadadadadadadadadad
w[3]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[4]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[5]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[6]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[7]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[0] と w[1] に無駄のない配置となり、構造体のサイズが3ワードに改善しました。
構造体にコメントすると下記のようなメモリ配置となります。
// テスト3のパディング構成
struct MyStruct {
// w[0]:1ワード目
uint256 val256;
// w[1]:2ワード目
uint160 val160;
uint64 val64;
uint32 val32;
// w[2]:3ワード目
address addr160;
uint16 val16;
uint8 val8;
// 余白 72 bits
}
まとめ
Solidity はワード長が大きいので、パディングによる影響を受けやすい環境だと思います。構造体の設計の際は、無駄のない構成を心がけてデータの肥大化を回避し、ひいては省ガスにつなげていきたいものです。
おまけ
構造体のサイズを半ワード未満にしたらどうなるでしょうか?
1ワード内に複数の構造体のインスタンスが配置されるのでしょうか?
淡い期待を込めて、下記の構造体でテストしてみました(※ソースコードはこちら)。
struct MyStruct {
uint8 val8;
}
// 要素4の配列
MyStruct[4] internal arrForCheck;
// 各要素にわかりやすい値を指定
constructor() public {
arrForCheck[0].val8 = uint8( 0x80 );
arrForCheck[1].val8 = uint8( 0x81 );
arrForCheck[2].val8 = uint8( 0x82 );
arrForCheck[3].val8 = uint8( 0x83 );
}
結果は下記となりました。
@---------------------
@ checkStructUint8
@---------------------
w[0]: 0000000000000000000000000000000000000000000000000000000000000080
w[1]: 0000000000000000000000000000000000000000000000000000000000000081
w[2]: 0000000000000000000000000000000000000000000000000000000000000082
w[3]: 0000000000000000000000000000000000000000000000000000000000000083
w[4]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[5]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[6]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
w[7]: ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
残念でした。
構造体1つにつき1ワード割り当てられています。
コメントを書くと下記のような感じですかね。
// おまけのパディング構成
struct MyStruct {
uint8 val8;
// 余白 248 bits
}
いくら頑張って構成要素を小さくしても、構造体のサイズは 256 bitを下回らないようです。構造体のメンバーがすでに1ワードに収まっているのなら、それ以上 小さくしてもストレージサイズとしては変化はないってことですね。
補足
この記事のテストは、Solidity のコンパイラ [0.4.x]系まででしか利用できない点をご了承ください(※[0.5]以降のコンパイラだと、未初期化の storage 変数はエラーとなってしまいコンパイルに通りません)。