【 storage vs memory 】イーサリアム上でのガス消費量の比較

## はじめに

**Solidity** を勉強していると、「 **storage** はコストがかかるから 極力 **memory** を使うべき 」という話をよく聞きます。ほかにも、構造体の内容を変更する際など、対象のデータを **memory** 変数に取り出して編集し、最後に実体となるデータに上書きするようなコードもよく見かけます。

```:例えばこんな感じ：お行儀のよいコード
// 更新したいデータをメモリ変数に取り出す
SomeData memory temp = datas[_target];

// 内容の変更
temp.gold = _gold;
temp.coin = _coin;
temp.exp = _exp;
temp.friend = _friend;

// 実データへ反映
datas[_target] = temp;
```

なるほど、**gold**, **coin**, **exp**, **friend** の値をメモリ上で処理することで、ストレージ領域への書き込みを最後の１回で済ませています。見た目にもわかりやすい書き方ですし、これでコスト（ガスの消費）が抑えられると言うわけですね。

…ですが、実際のところ、この書き方はどれほどの効果があるのでしょうか？

例えば、ストレージを直接まさぐるような下記のコードと比較して、どれぐらいのコストが抑えられるのでしょうか？

```:こんなコードはマナー違反？
datas[_target].gold = gold;
datas[_target].coin = coin;
datas[_target].exp = exp;
datas[_target].friend = friend;
```

```:変数をstorageで取り出して直接いじるのは高くつく？
SomeData storage temp = datas[_target];
temp.gold = _gold;
temp.coin = _coin;
temp.exp = _exp;
temp.friend = _friend;
```

実際のところどうなのでしょうか？　試しにテストしてみましょう。

## テストに関して

さて、データの構成もガス消費に影響すると思うので、テストに使う構造体を３タイプ用意します。それぞれ、**{A.変数の少ない１ワードの構造体}**、**{B.変数の少ない４ワードの構造体}**、**{C.変数の多い１ワードの構造体}**としてみました。

そして、上記構造体の動的配列をデータとしてもつコントラクトに、５つの検証用の関数を用意することにします。

**関数.1** … 追加要素を **memory** 変数に代入してデータ配列に **push** する
**関数.2** … データ配列の **length** を＋１して要素を増やし、**storage** 変数に取り出して代入する
**関数.3** … 既存のデータに対して直接値を更新する
**関数.4** … 既存のデータを **storage** 変数に取り出して値を更新する
**関数.5** … 既存のデータを **memory** 変数に取り出して編集した後で上書きする

これで「３タイプの構造体 × ５つの関数 ＝ １５種類」のガス消費が比較できる想定です。では、早速テストしていきましょう。
（※実際にテストしてみたい方は、[こちらのプロジェクト](https://github.com/hakumai-iida/SolidityStorageCheck)をご利用ください）
（※iOS アプリ上でのイーサリアムへの接続に関しては[こちらの記事](https://qiita.com/hakumai-iida/items/7167aeae5f9f96c92507)を参照ください）

### 構造体Ａ：変数の少ない１ワードの構造体


```:構造体Ａ：変数は４つ、サイズは１ワード（256bit）
struct MyData{
    uint128   serialNo;
    uint64    ownerId;
    uint32    maxPrice;
    uint32    minPrice;
}
```

**構造体Ａ**に対するテスト結果が下記となります（[ソースコードはこちら](https://github.com/hakumai-iida/SolidityStorageCheck/blob/master/sol/StorageCheckA.sol)）。

**・[関数Ａー１のガス消費：53,780](https://rinkeby.etherscan.io/tx/0xc72481d658263a33c47a968665ec9efa043ebbba72308dfeb0c9720de5ec6d0e)**（memory 変数の push によるデータ追加）
<details><summary>関数Ａー１のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ａー１：メモリ上でデータを作成したあとに追加
//-------------------------------------------
function createWithMemory( uint256 _val256 ) public{
    // メモリ上で値を設定
    MyData memory newData = MyData({
      serialNo:uint128(_val256),
      ownerId:uint64(_val256),
      maxPrice:uint32(_val256),
      minPrice:uint32(_val256)
    });

    // 設定したデータを追加
    arrData.push( newData );
}
```
</div></details>


**・[関数Ａー２のガス消費：55,885](https://rinkeby.etherscan.io/tx/0xc805943d9468cf0cddd75d3749aeecd6e187dcf329dbb0719a017dcf6642c74d)**（空要素を追加した後にstorage経由でデータ設定）
<details><summary>関数Ａー２のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ａー２：データを追加した後でストレージ上で設定
//-------------------------------------------
function createWithStorage( uint256 _val256 ) public{
    // 配列の要領を増やす（※末尾に空要素が追加される）
    arrData.length += 1;

    // ストレージ変数として取り出して設定
    MyData storage lastData = arrData[arrData.length-1];
    lastData.serialNo = uint128(_val256);
    lastData.ownerId = uint64(_val256);
    lastData.maxPrice = uint32(_val256);
    lastData.minPrice = uint32(_val256);
}
```
</div></details>


**・[関数Ａー３のガス消費：37,084](https://rinkeby.etherscan.io/tx/0xaaf573a873e730169f5eb0457baeb05fa6ea6391d2be6067550141453f28ca10)**（対象を直接いじるデータ更新）
<details><summary>関数Ａー３のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ａー３：データを直接更新
//-------------------------------------------
function updateDirect( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    arrData[_at].serialNo = uint128(_val256);
    arrData[_at].ownerId = uint64(_val256);
    arrData[_at].maxPrice = uint32(_val256);
    arrData[_at].minPrice = uint32(_val256);
}
```
</div></details>


**・[関数Ａー４のガス消費：34,414](https://rinkeby.etherscan.io/tx/0xc9882a873c71eeb3240950ebe9782e417a2ae94eb65b9efad662c252c7571628)**（storage 変数経由でのデータ更新）
<details><summary>関数Ａー４のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ａー４：ストレージ変数を利用して更新
//-------------------------------------------
function updateWithStorage( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    MyData storage storageData = arrData[_at];
    storageData.serialNo = uint128(_val256);
    storageData.ownerId = uint64(_val256);
    storageData.maxPrice = uint32(_val256);
    storageData.minPrice = uint32(_val256);
}
```
</div></details>


**・[関数Ａー５のガス消費：39,365](https://rinkeby.etherscan.io/tx/0xd0b1f8bc5234c8293ff8cfd0fea2fcf0baee50d1baad38815817e19375929088)**（memory 変数の代入によるデータ更新）
<details><summary>関数Ａー５のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ａー５：メモリ変数を利用して更新した後に上書き
//-------------------------------------------
function updateWithMemory( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    MyData memory memoryData = arrData[_at];
    memoryData.serialNo = uint128(_val256);
    memoryData.ownerId = uint64(_val256);
    memoryData.maxPrice = uint32(_val256);
    memoryData.minPrice = uint32(_val256);

    arrData[_at] = memoryData;
}
```
</div></details>


### 構造体Ｂ：変数の少ない４ワードの構造体


```:構造体Ｂ：変数は４つ、サイズは４ワード（1024bit）
struct MyData{
    uint256    userId;
    uint256    publicKey;
    uint256    hash;
    uint256    option;
}
```

**構造体Ｂ**に対するテスト結果が下記となります（[ソースコードはこちら](https://github.com/hakumai-iida/SolidityStorageCheck/blob/master/sol/StorageCheckB.sol)）。

**・[関数Ｂー１のガス消費：107,749](https://rinkeby.etherscan.io/tx/0x857cfc4a62bd37722fc56ec46dae317e24c2536730424e194c8954a4c8cbe5dc)**（memory 変数の push によるデータ追加）
<details><summary>関数Ｂー１のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｂー１：メモリ上でデータを作成したあとに追加
//-------------------------------------------
function createWithMemory( uint256 _val256 ) public{
    // メモリ上で値を設定
    MyData memory newData = MyData({
        userId:_val256,
        publicKey:_val256,
        hash:_val256,
        option:_val256
    });

    // 設定したデータを追加
    arrData.push( newData );
}
```
</div></details>


**・[関数Ｂー２のガス消費：109,934](https://rinkeby.etherscan.io/tx/0x183b77943c5d900c7cf6aad8c4292a98ee80e1e87db27553443b882b9e194824)**（空要素を追加した後にstorage経由でデータ設定）
<details><summary>関数Ｂー２のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｂー２：データを追加した後でストレージ上で設定
//-------------------------------------------
function createWithStorage( uint256 _val256 ) public{
    // 配列の要領を増やす（※末尾に空要素が追加される）
    arrData.length += 1;

    // ストレージ変数として取り出して設定
    MyData storage lastData = arrData[arrData.length-1];
    lastData.userId = _val256;
    lastData.publicKey = _val256;
    lastData.hash = _val256;
    lastData.option = _val256;
}
```
</div></details>


**・[関数Ｂー３のガス消費：46,166](https://rinkeby.etherscan.io/tx/0x374d304bf1fee7b9d3c5677504e8f4798b980f785382d9ddda53afa26f09af19)**（対象を直接いじるデータ更新）
<details><summary>関数Ｂー３のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｂー３：データを直接更新
//-------------------------------------------
function updateDirect( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    arrData[_at].userId = _val256;
    arrData[_at].publicKey = _val256;
    arrData[_at].hash = _val256;
    arrData[_at].option = _val256;
}
```
</div></details>


**・[関数Ｂー４のガス消費：43,451](https://rinkeby.etherscan.io/tx/0x088b30cceccdcd9a4bf0049b1c36b6e9a197b690037a4407812fb220e9a8f522)**（storage 変数経由でのデータ更新）
<details><summary>関数Ｂー４のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｂー４：ストレージ変数を利用して更新
//-------------------------------------------
function updateWithStorage( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    MyData storage storageData = arrData[_at];
    storageData.userId = _val256;
    storageData.publicKey = _val256;
    storageData.hash = _val256;
    storageData.option = _val256;
}
```
</div></details>


**・[関数Ｂー５のガス消費：47,943](https://rinkeby.etherscan.io/tx/0x689d5c88f8622e319ae79baf9327f335ec048cebad104d4aeef670b1b61abf07)**（memory 変数の代入によるデータ更新）
<details><summary>関数Ｂー５のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｂー５：メモリ変数を利用して更新した後に上書き
//-------------------------------------------
function updateWithMemory( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    MyData memory memoryData = arrData[_at];
    memoryData.userId = _val256;
    memoryData.publicKey = _val256;
    memoryData.hash = _val256;
    memoryData.option = _val256;

    arrData[_at] = memoryData;
}
```
</div></details>

### 構造体Ｃ：変数の多い１ワードの構造体


```:構造体Ｃ：変数は配列要素込みで２３、サイズは１ワード（256bit）

struct MyData{
    uint32    rscId;
    uint16    hp;
    uint16    mp;
    uint16    attack;
    uint16    guard;
    uint16    speed;
    uint16    luck;
    uint8[16] arrDNA;
}
```

**構造体Ｃ**に対するテスト結果が下記となります（[ソースコードはこちら](https://github.com/hakumai-iida/SolidityStorageCheck/blob/master/sol/StorageCheckC.sol)）。

**・[関数Ｃー１のガス消費：136,636](https://rinkeby.etherscan.io/tx/0x2e9d2b8960cee08f727291c05af7a243a9dfe57b60cfb853bcd29f4cc3468637)**（memory 変数の push によるデータ追加）
<details><summary>関数Ｃー１のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｃー１：メモリ上でデータを作成したあとに追加
//-------------------------------------------
function createWithMemory( uint256 _val256 ) public{
    // メモリ上で値を設定
    MyData memory newData = MyData({
        rscId:uint32(_val256),
        hp:uint16(_val256),
        mp:uint16(_val256),
        attack:uint16(_val256),
        guard:uint16(_val256),
        speed:uint16(_val256),
        luck:uint16(_val256),
        arrDNA:[
          uint8(_val256), uint8(_val256), uint8(_val256), uint8(_val256),
          uint8(_val256), uint8(_val256), uint8(_val256), uint8(_val256),
          uint8(_val256), uint8(_val256), uint8(_val256), uint8(_val256),
          uint8(_val256), uint8(_val256), uint8(_val256), uint8(_val256)
        ]
    });

    // 設定したデータを追加
    arrData.push( newData );
}
```
</div></details>


**・[関数Ｃー２のガス消費：109,583](https://rinkeby.etherscan.io/tx/0xa319441b7a35399ed45fe633a76feee950029688579e297d8c039c6869f5f4e8)**（空要素を追加した後にstorage経由でデータ設定）
<details><summary>関数Ｃー２のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｃー２：データを追加した後でストレージ上で設定
//-------------------------------------------
function createWithStorage( uint256 _val256 ) public{
    // 配列の要領を増やす（※末尾に空要素が追加される）
    arrData.length += 1;

    // ストレージ変数として取り出して設定
    MyData storage lastData = arrData[arrData.length-1];
    lastData.rscId = uint32(_val256);
    lastData.hp = uint16(_val256);
    lastData.mp = uint16(_val256);
    lastData.attack = uint16(_val256);
    lastData.guard = uint16(_val256);
    lastData.speed = uint16(_val256);
    lastData.luck = uint16(_val256);
    for( uint i=0; i<16; i++ ){
        lastData.arrDNA[i] = uint8( _val256 );
    }
}
```
</div></details>


**・[関数Ｃー３のガス消費：92,744](https://rinkeby.etherscan.io/tx/0x39c02b16fb92a7d38424da76a4b57c96f6f7931c974128d9aa41f0ca9ec8c31a)**（対象を直接いじるデータ更新）
<details><summary>関数Ｃー３のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｃー３：データを直接更新
//-------------------------------------------
function updateDirect( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    arrData[_at].rscId = uint32(_val256);
    arrData[_at].hp = uint16(_val256);
    arrData[_at].mp = uint16(_val256);
    arrData[_at].attack = uint16(_val256);
    arrData[_at].guard = uint16(_val256);
    arrData[_at].speed = uint16(_val256);
    arrData[_at].luck = uint16(_val256);
    for( uint i=0; i<16; i++ ){
        arrData[_at].arrDNA[i] = uint8(_val256);
    }
}
```
</div></details>


**・[関数Ｃー４のガス消費：73,100](https://rinkeby.etherscan.io/tx/0x7c6b3138c17aa2f96d451e317b71e6a85e44874daad82da389642f67598371e0)**（storage 変数経由でのデータ更新）
<details><summary>関数Ｃー４のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｃー４：ストレージ変数を利用して更新
//-------------------------------------------
function updateWithStorage( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    MyData storage storageData = arrData[_at];
    storageData.rscId = uint32(_val256);
    storageData.hp = uint16(_val256);
    storageData.mp = uint16(_val256);
    storageData.attack = uint16(_val256);
    storageData.guard = uint16(_val256);
    storageData.speed = uint16(_val256);
    storageData.luck = uint16(_val256);
    for( uint i=0; i<16; i++ ){
        storageData.arrDNA[i] = uint8(_val256);
    }
}
```
</div></details>


**・[関数Ｃー５のガス消費：127,765](https://rinkeby.etherscan.io/tx/0xe375da21f58afaecdc00f4f3b71641d49c072546f924779a6b93ff7cdcb19c68)**（memory 変数の代入によるデータ更新）
<details><summary>関数Ｃー５のコード（クリックで開きます）</summary><div>

```
//-------------------------------------------
// 関数Ｃー５：メモリ変数を利用して更新した後に上書き
//-------------------------------------------
function updateWithMemory( uint256 _at, uint256 _val256 ) public{
    require( _at >= 0 && _at < arrData.length );

    MyData memory memoryData = arrData[_at];
    memoryData.rscId = uint32(_val256);
    memoryData.hp = uint16(_val256);
    memoryData.mp = uint16(_val256);
    memoryData.attack = uint16(_val256);
    memoryData.guard = uint16(_val256);
    memoryData.speed = uint16(_val256);
    memoryData.luck = uint16(_val256);
    for( uint i=0; i<16; i++ ){
        memoryData.arrDNA[i] = uint8(_val256);
    }

    arrData[_at] = memoryData;
}
```
</div></details>

## 意外な結果？
…あれ？

個人的には意外な結果になりました（※テスト自体は複数回行ったのですが、すべての結果で値が同じだったので誤差といこともないと思います）。

まず、データの追加をする**関数１**と**関数２**の結果を見ると、メンバー変数が少ない**構造体Ａ**と**構造体Ｂ**の場合、**memory** 経由の処理が若干優位となっており、これは予想通りです（※とは言え期待したほどの差はでていません）。

逆に、メンバー変数の多い**構造体Ｃ**の場合、**memory** 経由の処理である**関数１**が、**strage** 経由の**関数２**に比べて、２０％以上ガスを浪費してしまっています。

そして、予想外だったのが、すべての構造体に対して、**memory** 経由でデータを更新する**関数５**のガス消費量が、**関数３**と**関数４**よりも多くなってしまていることです。さらに驚くべきことに、微増程度で済んでいる**構造体Ａ**と**構造体Ｂ**に比べて**構造体Ｃ**の消費量が、**関数３**に対して４０％弱、**関数４**に対しては７０％以上も増えてしまっている点です。

これはどうしたことでしょうか？
怪しむべきはやはりメンバー変数の数ということになりそうです。

## memory 経由で読み書きする裏側

私の認識では、**memory** と **storage** 間における構造体の読み書きは、**256 bit** のワード単位で行われると思っていました。メンバー変数がいくつ存在しても、構造体のサイズが１ワードに収まっているならば、１ワード分のコストで入出力されるのではないかと…。

しかし、この認識は誤っていたようです。
テスト結果から、メンバー変数が多いほどオーバヘッドがでているのは明らかです。

では、**関数Ｃー５**の内部では何が起こっているのでしょう？

メモリに構造体のデータを取り出している下記のコードを、アセンブリで確認してみましょう。

```
MyData memory memoryData = arrData[_at];
```

<details><summary>アセンブリ（クリックで開きます）
</summary><div>

```:関数Ｃー５の抜粋〜memoryデータの取り出し部分〜
        /* "StorageCheckC.sol":4023:4062  MyData memory memoryData = arrData[_at] */
      mload(0x40)
      dup1
      0x0100
      add
      0x40
      mstore
      swap1
      dup2
      0x00
      dup3
      add
      0x00
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xffffffff
      and
      0xffffffff
      and
      0xffffffff
      and
      dup2
      mstore
      0x20
      add
      0x00
      dup3
      add
      0x04
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xffff
      and
      0xffff
      and
      0xffff
      and
      dup2
      mstore
      0x20
      add
      0x00
      dup3
      add
      0x06
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xffff
      and
      0xffff
      and
      0xffff
      and
      dup2
      mstore
      0x20
      add
      0x00
      dup3
      add
      0x08
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xffff
      and
      0xffff
      and
      0xffff
      and
      dup2
      mstore
      0x20
      add
      0x00
      dup3
      add
      0x0a
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xffff
      and
      0xffff
      and
      0xffff
      and
      dup2
      mstore
      0x20
      add
      0x00
      dup3
      add
      0x0c
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xffff
      and
      0xffff
      and
      0xffff
      and
      dup2
      mstore
      0x20
      add
      0x00
      dup3
      add
      0x0e
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xffff
      and
      0xffff
      and
      0xffff
      and
      dup2
      mstore
      0x20
      add
      0x01
      dup3
      add
      0x10
      dup1
      0x20
      mul
      mload(0x40)
      swap1
      dup2
      add
      0x40
      mstore
      dup1
      swap3
      swap2
      swap1
      dup3
      0x10
      dup1
      iszero
      tag_49
      jumpi
      0x20
      mul
      dup3
      add
      swap2
      0x00
      swap1
    tag_50:
      dup3
      dup3
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xff
      and
      0xff
      and
      dup2
      mstore
      0x20
      add
      swap1
      0x01
      add
      swap1
      0x20
      dup3
      0x00
      add
      div
      swap3
      dup4
      add
      swap3
      0x01
      sub
      dup3
      mul
      swap2
      pop
      dup1
      dup5
      gt
      tag_50
      jumpi
      swap1
      pop
```
</div></details>

さて、アセンブリを眺めると、先頭から少ししたところに、なにやら **32 bit** の値をいじくるコードが見受けられます…。

```
      0xffffffff
      and
      0xffffffff
      and
      0xffffffff
```

そして、その少し下のあたりから、**16 bit** の値をこねくるコードが、１、２、３、４… 合計で６ブロック出現します。

```
      0xffff
      and
      0xffff
      and
      0xffff
```

…この並びは、構造体Ｃのメンバー変数の型の並びと合致します。
となると、下記のコードは **uint8[16] arrDNA** の値を処理するループ部であることがわかります。

```
    tag_50:
      dup3
      dup3
      swap1
      sload
      swap1
      0x0100
      exp
      swap1
      div
      0xff
      and
      0xff
      and
```

なんということでしょう、構造体のデータを **memory** 変数として読み込む際は、構造体のメンバーが個別に読み込まれているではありませんか！

そして、**memory** 変数をストレージデータへ書き戻す下記のコードでも、同様の処理が行われています（[アセンブリコードの全体はこちら](https://github.com/hakumai-iida/SolidityStorageCheck/blob/master/sol/asm/StorageCheckC.evm)）。

```
arrData[_at] = memoryData;
```

これで謎は解けました。

**memory** 変数に構造体を取り出す ＆ 元のデータに書き戻す際の、メンバー変数の読み書きのオーバーヘッドにより、**構造体Ｃ** に対する処理のガス消費量が跳ね上がっていた訳です。

**memory** 変数経由で構造体の値を更新しようと思った際は、メンバー変数ごとにメモリへの読み込み ＆ ストレージへの書き戻しが行われ、バカにならないレベルでガスを浪費させてしまうことを、肝に命じておきましょう。

## まとめ

今回、３つの構造体でテストしてみました。

まず、**構造体Ａ**と**構造体Ｂ**はメンバー変数の数が同じでワードサイズが１と４という違いによる比較でした。結果、ワードサイズの大きい**構造体Ｂ**のほうがコスト（ガス消費量）が多くなりましたが、これは、予想通りといえるでしょう。

一方で、メンバー変数の数による比較では劇的な差が出ました。

２３個のケース（**構造体Ｃ**）では **memory** 変数経由の処理が惨敗でしたが、４つのケース（**構造体Ａ**／**Ｂ**）では、各種の値がほぼほぼ均衡した結果となりました。これは憶測ですが、メンバー変数が３つ／２つの場合、もしくは、代入以外の脂っこい処理が書き加わった状況であれば、**memory** 経由の処理の方が、コスト的に優位になってくると思います。

さて、ガスの消費は手数料に直結します。
だからこそ **storage vs memory** の悩みはつきそうにありません。

どちらを選択するか迷った時は、多少面倒でも２通りの処理を書いてみて、[イーサスキャン](https://etherscan.io/)で比較してみましょう。これが一番手っ取り早くて確実な答えを与えてくれるはずです。