[Solidityのはじめ方] Part15: ストレージとメモリの違い

Solidity

スマートコントラクトのデータ管理の要!ガス代にも影響大!

Solidityでスマートコントラクトを開発する上で、データの保存場所を理解することは非常に重要です。特に、storage(ストレージ)とmemory(メモリ)は、その性質とコストが大きく異なるため、適切に使い分ける必要があります。この違いを理解することは、効率的で安全なコントラクトを作成するための第一歩です。🚶‍♀️

1. ストレージ (Storage) とは? 🗄️

ストレージは、コントラクトの状態変数が永続的に保存される場所です。コンピュータのハードディスクに例えられます。一度ストレージに書き込まれたデータは、トランザクションが完了した後もブロックチェーン上に残り続けます。

  • 永続性: ブロックチェーン上に永続的に記録されます。
  • 場所: 各コントラクトが持つ固有のデータ領域です。
  • デフォルト: 関数外で宣言された状態変数(State Variables)は、デフォルトでストレージに格納されます。
  • コスト: 読み書きにはガス代がかかり、特に書き込み(SSTOREオペコード)は非常に高コストです。

ストレージは、コントラクトの状態(例:トークンの残高、所有者アドレスなど)を保持するために不可欠です。

pragma solidity ^0.8.0;

contract StorageExample {
    // 状態変数はデフォルトでストレージに保存される
    uint256 public myNumber;
    address public owner;
    mapping(address => uint256) public balances;

    struct User {
        string name;
        uint age;
    }
    User[] public users; // 構造体の配列もストレージ

    constructor() {
        myNumber = 100;
        owner = msg.sender;
    }

    function updateNumber(uint256 _newNumber) public {
        // ストレージ変数 'myNumber' を更新
        myNumber = _newNumber; // SSTOREオペコードが実行され、ガス代がかかる
    }

    function addUser(string memory _name, uint _age) public {
        // ストレージ配列 'users' に新しい要素を追加
        // この操作もストレージへの書き込みとなる
        users.push(User(_name, _age));
    }
}

2. メモリ (Memory) とは? 💡

メモリは、関数実行中の一時的なデータ保存場所です。コンピュータのRAMに例えられます。関数が実行を終了すると、メモリに保存されていたデータは破棄されます。

  • 永続性: 一時的。関数実行終了時にクリアされます。
  • 場所: コントラクトの実行環境(EVM)が確保する一時領域です。
  • デフォルト: 関数の引数や関数内で宣言されたローカル変数(構造体、配列、文字列など参照型)は、明示的に指定しない限りメモリに格納されます(memoryキーワード)。値型(uint, bool, addressなど)のローカル変数は通常スタックに置かれますが、メモリが使われることもあります。
  • コスト: ストレージに比べて読み書きのガス代ははるかに安価です。ただし、メモリの確保(拡張)にはコストがかかります。

メモリは、関数内での一時的な計算や、ストレージから読み込んだデータを加工する際などに利用されます。

pragma solidity ^0.8.0;

contract MemoryExample {
    uint256[] public numbers = [10, 20, 30]; // ストレージ変数

    // 関数引数はデフォルトで memory (外部関数の場合)
    function calculateSum(uint256[] memory _array) public pure returns (uint256) {
        uint256 sum = 0; // ローカル変数 (スタック)

        // メモリ配列をループ処理
        for (uint i = 0; i < _array.length; i++) {
            sum += _array[i];
        }
        // _array と sum は関数終了時に破棄される
        return sum;
    }

    function processNumbers() public view returns (uint256[] memory) {
        // ストレージ配列をメモリにコピー
        uint256[] memory tempArray = numbers;

        // メモリ上で配列を操作(ストレージには影響しない)
        for (uint i = 0; i < tempArray.length; i++) {
            tempArray[i] = tempArray[i] * 2;
        }

        // 加工したメモリ配列を返す (このデータも一時的)
        return tempArray;
    }

    function getStorageDataInMemory() public view {
        // ストレージへの参照をメモリに作成 (storageポインタ)
        // uint256[] storage storageRef = numbers; // これは storage へのポインタ

        // ストレージからメモリへコピー
        uint256[] memory memoryCopy = numbers;
        // memoryCopyを変更しても numbers には影響しない
        memoryCopy[0] = 999;
    }
}
注意点: memoryで宣言された変数にストレージ変数を代入すると、データのコピーが作成されます。一方、storageキーワードを使ってローカル変数を宣言すると、それはストレージへのポインタ(参照)となり、その変数を変更すると元のストレージデータも変更されます。
// メモリへのコピー
uint256[] memory memoryCopy = storageArray;
memoryCopy[0] = 1; // storageArray[0] は変更されない

// ストレージへのポインタ
uint256[] storage storagePointer = storageArray;
storagePointer[0] = 1; // storageArray[0] が 1 に変更される

3. 主な違いのまとめと比較表 📊

ストレージとメモリの主な違いをまとめます。

特徴ストレージ (Storage)メモリ (Memory)
永続性永続的 (ブロックチェーンに記録)一時的 (関数実行中のみ)
場所コントラクト固有の永続領域EVMの一時実行領域
デフォルト状態変数関数の引数、関数内の参照型ローカル変数
ガス代 (コスト)高い (特に書き込み: SSTORE)安い (ただし確保・拡張にコスト)
主な用途コントラクトの状態保持 (残高、所有権など)一時的な計算、データ加工、関数引数
データ変更直接変更可能、ポインタ経由で変更可能コピーを作成して変更 (元のストレージには影響しない)

4. ガス代と最適化 🔥

ストレージ操作、特に書き込み(SSTORE)は、Solidityで最もガス代が高くなる操作の一つです。そのため、ガス代を最適化するには、ストレージへのアクセスを最小限に抑えることが重要になります。

  • ストレージ読み書きの最小化: ループ内でストレージ変数を何度も読み書きするのは避けましょう。必要な値を一度メモリ上のローカル変数にコピーし、計算を行ってから、最後に一度だけストレージに書き戻す方が効率的です。
  • メモリの活用: 関数内の一時的なデータや計算結果は、可能な限りメモリを使用します。
  • calldataの利用: 外部関数(external)の参照型の引数(配列や構造体など)で、関数内で変更する必要がない場合は、memoryの代わりにcalldataを使用することを検討します。calldataは関数の入力データが格納される読み取り専用の領域で、メモリへのコピーが発生しないため、ガス代を節約できます。
  • 構造体と配列の扱い: 大きな構造体や配列をストレージからメモリにコピーするのはコストがかかる場合があります。必要な要素だけを読み取るか、ストレージポインタ(storageキーワード付きローカル変数)をうまく利用する(ただし、意図しないストレージ変更に注意)などの工夫が考えられます。
pragma solidity ^0.8.0;

contract GasOptimization {
    uint256 public totalSum;
    uint256[] public data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];

    // 非効率な例: ループ内で毎回ストレージアクセス
    function calculateSumInefficient() public {
        for (uint i = 0; i < data.length; i++) {
            // 毎回ストレージから読み込み (SLOAD)
            // 毎回ストレージへ書き込み (SSTORE) - 非常に高コスト!
            totalSum = totalSum + data[i];
        }
    }

    // 効率的な例: メモリを活用
    function calculateSumEfficient() public {
        // 状態変数をメモリに読み込む (SLOAD x 1)
        uint256[] memory localData = data;
        uint256 sum = totalSum; // これも一度読み込む

        // メモリ上で計算
        for (uint i = 0; i < localData.length; i++) {
            sum = sum + localData[i];
        }

        // 最後に一度だけストレージに書き込む (SSTORE x 1)
        totalSum = sum;
    }

    // calldataの使用例
    // _inputDataは変更されないのでcalldataが効率的
    function processCalldata(uint256[] calldata _inputData) external pure returns (uint256) {
        uint256 sum = 0;
        for (uint i = 0; i < _inputData.length; i++) {
            sum += _inputData[i];
        }
        return sum;
    }
}

このように、storagememory(そしてcalldata)の特性を理解し、適切に使い分けることが、ガス効率の良いスマートコントラクト開発の鍵となります。🔑

まとめ

  • ストレージ (Storage): ブロックチェーン上に永続保存。状態変数のデフォルト。ガス代が高い(特に書き込み)。
  • メモリ (Memory): 関数実行中の一時保存。関数引数やローカル参照変数のデフォルト。ガス代は比較的安い。
  • ガス最適化のため、ストレージアクセスは最小限にし、メモリやcalldataを有効活用しましょう。
  • storageポインタとmemoryコピーの違いを理解することが重要です。

これで、ストレージとメモリの違い、そしてそれぞれの使いどころが理解できたはずです。次のステップでは、ガス最適化のテクニックについてさらに深く学んでいきましょう!🚀

参考情報

コメント

タイトルとURLをコピーしました