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

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

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コピーの違いを理解することが重要です。

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

参考情報

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です