忙しい人のための要約
- Solidityの整数リテラルは「その値が収まる、最も小さい型」になるようです。
- 通常は(たぶん)気にしなくて良いですが、ビットシフト演算をするとハマる時があります。
富豪的プログラミングとの別れ
Ethereumとかブロックチェーンの世界では富豪的プログラミングは一旦忘れなきゃいけない気がしました。
というのはEthereumではストレージの状態変更はコストが高いのです。ここで言うコストとは時間とかメモリとかのことではなくてEther(=お金)のことです。まあみんなのメモリを消費するのでお金を取るという発想なのですが。
例えばゼロから非ゼロへの書換えは20000gas掛かります。 https://etherscan.io/chart/gasprice によると2018年2月22日現在の平均gasPriceは17902757911 Wei = 0.000000017902757911 Ether。10万円/1Etherとすると0.00179027579円。まあ微々たるものですが、非ゼロのストレージの書換えは5000gasなので1/4のお金で済みます。
貧民的プログラミング
例えば、なにか整数型のidに対してtrue/falseな状態を管理したいと思います。例えば出勤状態とか。通常はmappingを使います。
mapping (uint256 => bool) idToBool;
しかし、これはfalseからtrueにするたびに20000gasも掛かってしまうのです。社員が1000人いれば20000000gasです!状態変更に伴うストレージの操作をゼロから非ゼロの20000gasではなく、非ゼロからの書換えの5000gasにできれば良いのに…と考えてしまいます。
なお、true->falseに書き換えると5000gas払った後に最大15000gasのrefund(返金)があります。(が、一旦忘れます)
ビット演算
なので、古代行われていたと言われるビット演算を復活させたいと思いました。uint256で256ビットなので、idを256で割った商ごとにuint256を1個持ち、余り+1桁目のビットが立っていればtrueということにします。なんか頭が良くなった気がします!
pragma solidity ^0.4.19;
contract BoolMap {
mapping (uint256 => uint256) private idTables;
function _toMask(uint256 _value) private pure returns (uint256) {
return 1 << _value;
}
function put(uint256 _id, bool _value) public {
uint256 tableIndex = _id / 256;
uint256 idTable = idTables[tableIndex];
uint256 mask = _toMask(_id % 256);
if (_value) {
if ((idTable & mask) == 0) {
idTables[tableIndex] = idTable | mask;
}
} else {
if ((idTable & mask) != 0) {
idTables[tableIndex] = idTable & ~mask;
}
}
}
function get(uint256 _id) public view returns(bool) {
uint256 idTable = idTables[_id / 256];
uint256 mask = _toMask(_id % 256);
return (idTable & mask) != 0;
}
}
途中から動かなくなる
するとput(7, true)までは動いていましたが、put(8, true)から状態変更ができなくなりました。というのは若干嘘で、255とかの境界っぽいところが動かなくておかしいなあと思いましたが根本的にもっと小さいところがダメでした… 残念ながら頭は良くなっていませんでしたね…
原因
デバッガで調べていたりしましたがよく見たらコンパイラの警告が出ていました。
browser/BoolMap.sol:7:16: Warning: Result of shift has type uint8 and thus might overflow. Silence this warning by converting the literal to the expected type.
return 1 << _value;
^---------^
uint8をビットシフトしたらオーバーフローするんじゃないかと言われました。なるほどそうなのか…
対処
_toMask関数で1をuint256にキャストしたら動きました。
function _toMask(uint256 _value) private pure returns (uint256) {
return uint256(1) << _value;
}
結論と教訓
- ビットシフトするときは型に気を付けよう。
- というか 1 << n なんてよくやりそうだけどだいたい間違いだぞ。
- 小賢しい最適化はもうすこしSolidityの気持ちがわかるようになってからにしよう。
- コンパイラの警告はちゃんと読もう。