Solidity 構造体のパディングを意識して ストレージを効率化

## はじめに

いきなりですが、 [**CryptoKitties**](https://etherscan.io/address/0x06012c8cf97bead5deae237070f9587f8e7a266d#code) の **Kitty** 構造体を見てみましょう。

```solidity
// 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 で構造体を取り扱う機会は少なくありません。そして、その際に理解しておきたいのが構造体のパディングに関してです。

## 構造体のパディング

パディングとは、複数の変数を１つにまとめて取り扱う際、メンバーとなる変数がワード境界をまたがないようにするために、メモリ上に挿入される余白のことです。

一方で、この余白による落とし穴が、構造体のぱっと見のサイズと、メモリ上におけるサイズとの乖離が引き起こす「なんでこんなにストレージ喰ってるの？」問題です。

特に Solidity の場合、ワード長が 256 bit と大きいので、パデイングの影響がバカになりません。
では実際のところどうなのか？　ためしにテストしてみました。

## テストについて

テストに際して、**「EVMにおいて、未初期化の storage 変数はコントラクトの slot[0] を参照する」**という、ちょっと面白いクセを利用します。

具体的には、以下のような構造体を準備し、

```solidity
struct Word8 {
    uint256 word0;
    uint256 word1;
    uint256 word2;
    uint256 word3;
    uint256 word4;
    uint256 word5;
    uint256 word6;
    uint256 word7;
}
```

下記のコードにて、コントラクトのストレージ先頭領域を、**word8** 変数経由で参照して、構造体のパディングの様子を眺めようというものです。

```solidity
// 未初期化の word8 変数は slot[0](structForCheck) を参照する
Word8 storage word8;
```

### テスト１：何気ない並びにおけるパディング

まずは、変数を型順に、何気なく並べた構造体でテストしてみましょう。

```solidity
struct MyStruct {
    address addr160;
    uint256 val256;
    uint160 val160;
    uint64  val64;
    uint32  val32;
    uint16  val16;
    uint8   val8;
  }
```

[テストコード](https://github.com/hakumai-iida/SolidityStructTest/blob/master/sol/StructSampleBasic.sol)上、この構造体のインスタンスをコントラクトの一番最初のメンバーとして配置し、チェックがしやすいようにコンストラクタにて構造体の各要素にダミー値を設定しておきます。

```solidity
// structForCheck は word8 から参照される
MyStruct internal structForCheck;
```

```solidity
// コンストラクタ
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** の内容が参照できるようになりました（※[ソースコードはこちら](https://github.com/hakumai-iida/SolidityStructTest/blob/master/sol/StructSampleBasic.sol)）。
*（※実際にテストしてみたい方は、[こちらのプロジェクト](https://github.com/hakumai-iida/SolidityStructTest)をご利用ください）*
*（※iOS アプリ上でのイーサリアムへの接続に関しては[こちらの記事](https://qiita.com/hakumai-iida/items/7167aeae5f9f96c92507)を参照ください）*

テスト結果は下記となります。

```
@---------------------
@ 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]** の４ワードが構造体の領域＝メモリ上のサイズであることが見て取れます*（※[ff]の領域は structForCheck 変数の下に配置した番兵の値となります）*。

**[00]** の値の部分がパディング領域となり、**w[0]** の領域に **96 bit**、**w[3]** の領域に **232 bit** の余白が追加されていることがわかります。

構造体にコメントすると下記のようなメモリ配置となります。

```solidity
// テスト１のパディング構成
struct MyStruct {
    // w[0]：１ワード目
    address addr160;
    // 余白 96 bits

    // w[1]：２ワード目
    uint256 val256;

    // w[2]：３ワード目
    uint160 val160;
    uint64  val64;
    uint32  val32;

    // w[3]：４ワード目
    uint16  val16;
    uint8   val8;
    // 余白 232 bits
}
```

### テスト２：最悪の並びにおけるパディング

つづいて、変数の並びをバイトサイズ順に調整した後、気まぐれで **uint8** を１番上に移動した構造体と、そのテスト結果を見てみましょう（※[ソースコードはこちら](https://github.com/hakumai-iida/SolidityStructTest/blob/master/sol/StructSampleWorst.sol)）。

```solidity
  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
```

構造体のサイズが１つ増えて５ワードになってしまいました。
**w[0]** の領域に **248 bit**、**w[2]** の領域に **96 bit**、**w[4]** の領域に **240 bit** の余白ができてしまっています。

構造体にコメントすると下記のようなメモリ配置となります。

```solidity
// テスト２のパディング構成
struct MyStruct {
    // w[0]：１ワード目
    uint8   val8;
    // 余白 248 bits

    // w[1]：２ワード目
    uint256 val256;

    // w[2]：３ワード目
    address addr160;
    // 余白 96 bits

    // w[3]：４ワード目
    uint160 val160;
    uint64  val64;
    uint32  val32;

    // w[4]：５ワード目
    uint16  val16;
    // 余白 240 bits
}
```

### テスト３：ベストの並びにおけるパディング

ここまでのテストでお気づきと思いますが、パディングは空き領域に対して収まりの悪い変数が出現した際に発生します。逆に言うと、できるだけ空き領域を活用するように変数を並べることで、パディングを最小限にすることができます。

テスト１のケースを改めて見てみましょう。
**w[3]** の領域に **address addr160 (160 bit)** が収まるだけの余白があります。なので、**addr160** を **uint16 val16** と **uint8 val8** と同じ領域になるよう調整することで１ワード分改善できそうです。
*（※別の見方として、 w[0] に uint16 val16 と uint8 val8 の収まる余白があると考えて、val16 と val8 を構造体の上部へ移動するのでも、もちろんＯＫです）*

実際に調整した構造体とテスト結果が下記となります（※[ソースコードはこちら](https://github.com/hakumai-iida/SolidityStructTest/blob/master/sol/StructSampleFit.sol)）。

```solidity
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]** に無駄のない配置となり、構造体のサイズが３ワードに改善しました。

構造体にコメントすると下記のようなメモリ配置となります。

```solidity
// テスト３のパディング構成
struct MyStruct {
    // w[0]：１ワード目
    uint256 val256;

    // w[1]：２ワード目
    uint160 val160;
    uint64  val64;
    uint32  val32;

    // w[2]：３ワード目
    address addr160;
    uint16  val16;
    uint8   val8;
    // 余白 72 bits
}
```

## まとめ

Solidity はワード長が大きいので、パディングによる影響を受けやすい環境だと思います。構造体の設計の際は、無駄のない構成を心がけてデータの肥大化を回避し、ひいては省ガスにつなげていきたいものです。

## おまけ
構造体のサイズを半ワード未満にしたらどうなるでしょうか？
１ワード内に複数の構造体のインスタンスが配置されるのでしょうか？

淡い期待を込めて、下記の構造体でテストしてみました（※[ソースコードはこちら](https://github.com/hakumai-iida/SolidityStructTest/blob/master/sol/StructSampleUint8.sol)）。

```solidity
  struct MyStruct {
    uint8   val8;
  }

  // 要素４の配列
  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
```

残念でした。
構造体１つにつき１ワード割り当てられています。

コメントを書くと下記のような感じですかね。

```solidity
// おまけのパディング構成
struct MyStruct {
    uint8   val8;
    // 余白 248 bits
}
```

いくら頑張って構成要素を小さくしても、構造体のサイズは 256 bitを下回らないようです。構造体のメンバーがすでに１ワードに収まっているのなら、それ以上 小さくしてもストレージサイズとしては変化はないってことですね。

## 補足
*この記事のテストは、Solidity のコンパイラ [0.4.x]系まででしか利用できない点をご了承ください（※[0.5]以降のコンパイラだと、未初期化の　storage 変数はエラーとなってしまいコンパイルに通りません）。*