スマートコントラクトのデータ管理の要!ガス代にも影響大!
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; }
}
このように、storage
とmemory
(そしてcalldata
)の特性を理解し、適切に使い分けることが、ガス効率の良いスマートコントラクト開発の鍵となります。
まとめ
- ストレージ (Storage): ブロックチェーン上に永続保存。状態変数のデフォルト。ガス代が高い(特に書き込み)。
- メモリ (Memory): 関数実行中の一時保存。関数引数やローカル参照変数のデフォルト。ガス代は比較的安い。
- ガス最適化のため、ストレージアクセスは最小限にし、メモリや
calldata
を有効活用しましょう。 storage
ポインタとmemory
コピーの違いを理解することが重要です。
これで、ストレージとメモリの違い、そしてそれぞれの使いどころが理解できたはずです。次のステップでは、ガス最適化のテクニックについてさらに深く学んでいきましょう!
参考情報
- Solidity 公式ドキュメント – Data Location: https://docs.soliditylang.org/en/latest/types.html#data-location
- Solidity by Example – Data Locations: https://solidity-by-example.org/data-locations/