お疲れさまです。 次世代ビジネス推進部の荻原です。
最近、業務でスマコン開発を行うことになったのですが、その実装においてSolidityならではとも言える、頭を悩ませることになった仕様がありました。 それは、コントラクト内で複数のデータを持ちたいときに配列とmappingのどちらを利用するかというものです。
それらの実装方法は計算量が異なりTransaction処理にかかるGasに差が出てくるので、どのくらい違いがあるのか調べてみました。 その内容について紹介します。
目次
- Solidityの配列とmapping
- 検証の内容
- 検証結果
- ソースコード
- 参考
1. Solidityの配列とmapping
Solidityの配列は他の多くの言語と同様に長さがあり、配列ごと取得したりfor文で回したいときに便利です。
一方、mappingはkeyとvalueのペアでデータを保存します。 他の言語で言う辞書のようなものでhashテーブルのように動作しますが、長さを持たずどのようなkeyを指定しても初期値としてvalueの型の初期値が返ります。(例えばvalueがuintであれば0、stringであれば空文字が返ります。) そのため、keyからvalueの検索は速いですがvalueからkeyを探すことはできません。また、keyが存在しているかを判定するというようなこともできません。
補足ですが、実装の工夫次第ではmappingに追加した要素数やmappingに追加したkeyの一覧をコントラクト内で持っておくというようなことは可能です。(参考)
2. 検証の内容
許可をしたEOAのみが実行できる関数を考えます。 許可したEOAのaddressを
- 配列で持つ
- keyがaddress、valueがboolのmappingで持つ
それぞれの場合について違いを見てみます。
modifier onlyOperator() {
require(isOperator(msg.sender), "Caller is not the operator");
_;
}
function execute(bool flag) public onlyOperator {
_flag = flag;
}
execute()という関数は許可されたEOA(operator)しか実行することができません。
isOperator()は指定したaddressがoperatorかどうかをboolで返します。
この
isOperator()の実装が配列とmappingで変わります。
配列
配列ではfor文を用いて最大で配列の要素全件を線形探索する必要があります。
address[] private _operators;
function isOperator(address addr) public view returns (bool) {
for (uint256 i = 0; i < _operators.length; i++) {
if (_operators[i] == addr) {
return true;
}
}
return false;
}
mapping
mappingはハッシュ探索なのでkeyを指定するだけです。
mapping(address => bool) private _operators;
function isOperator(address addr) public view returns (bool) {
return _operators[addr];
}
その他にも例えば配列を使っているとoperatorのEOAがわからなくても配列をそのまま返すことで確認ができますが、mappingではそのような実装はできません。
3. 検証結果
operatorとして1個、10個、20個のaddressが保存されているそれぞれの場合で、
execute()の実行で消費されたGasがどの程度異なるかを配列とmappingで比較しました。
配列はaddressの配置によってfor文のループ回数が変わり、Gasが変わるので最大値として配列の末尾に探索対象のaddressが入っていることを想定しました。
また、
execute()で_flagをfalseからtrueにする場合とその反対の処理をする場合でもGasが変わるのでfalseからtrueにする場合で統一しています。
ネットワークはGanacheを用い、Truffleを利用してスマコンメソッドの実行を行いました。
% ganache --version
ganache v7.7.3 (@ganache/cli: 0.8.2, @ganache/core: 0.8.2)
% truffle version
Truffle v5.4.26 (core: 5.4.26)
Solidity - 0.8.19 (solc-js)
Node v16.10.0
Web3.js v1.5.3
| 実装方法/operator数 | 1 | 10 | 20 |
|---|---|---|---|
| mapping | 28772 | 28772 | 28772 |
| 配列 | 31055 | 54059 | 79619 |

当然ですが、mappingはoperatorがいくつになってもGasは一定です。 一方配列は見事にoperator数に比例してGasが増えています。
今回のoperator数では考慮するほどではないかもしれませんが、ガス代や処理速度を重視するようであればmappingで実装する方が良さそうです。
4. ソースコード
非常に簡易的な実装ですが、今回検証に使用した全ソースコードです。
配列
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract ListTest {
address private _owner;
bool private _flag;
address[] private _operators;
modifier onlyOwner() {
require(msg.sender == _owner, "Caller is not the owner");
_;
}
modifier onlyOperator() {
require(isOperator(msg.sender), "Caller is not the operator");
_;
}
constructor() {
_owner = msg.sender;
}
function isOperator(address addr) public view returns (bool) {
for (uint256 i = 0; i < _operators.length; i++) {
if (_operators[i] == addr) {
return true;
}
}
return false;
}
function setOperators(address[] memory addrs) public onlyOwner {
_operators = addrs;
}
function execute(bool flag) public onlyOperator {
_flag = flag;
}
}
mapping
// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;
contract MappingTest {
address private _owner;
bool private _flag;
mapping(address => bool) private _operators;
modifier onlyOwner() {
require(msg.sender == _owner, "Caller is not the owner");
_;
}
modifier onlyOperator() {
require(isOperator(msg.sender), "Caller is not the operator");
_;
}
constructor() {
_owner = msg.sender;
}
function isOperator(address addr) public view returns (bool) {
return _operators[addr];
}
function setOperator(address addr, bool flag) public onlyOwner {
_operators[addr] = flag;
}
function execute(bool flag) public onlyOperator {
_flag = flag;
}
}
5. 参考
荻原菜穂