之前遇到有朋友这么问:“为什么bool比uint256消耗更多的gas”。首先要说明的是这个说法是不准确的,并不是简单的bool和uint256这个类型差异造成的。我们在看OpenZeppelin的ReentrancyGuard.sol的源码时,会看到下面的写法,我们从底层机制上分析
// Booleans are more expensive than uint256 or any type that takes up a full
// word because each write operation emits an extra SLOAD to first read the
// slot's contents, replace the bits taken up by the boolean, and then write
// back. This is the compiler's defense against contract upgrades and
// pointer aliasing, and it cannot be disabled.
//.....
uint256 private constant NOT_ENTERED = 1;
uint256 private constant ENTERED = 2;
一. 首先,slot存储槽基本最小长度是256bit,EVM每次会按照整个slot进行读取,这个可以在instructions.go中找到“opSload”方法。
https://github.com/ethereum/go-ethereum/blob/master/core/vm/instructions.go
val := interpreter.evm.StateDB.GetState(scope.Contract.Address(), hash)
// 此处就是从slot中获取值,并不会判断变量的类型,直接返回整个32字节的内容
二. bool是1bit,如果是连续定义多个bool变量,编译器会把多个bool变量打包进同一个slot,用offset来定义每个变量的在slot中的位置。我们可以编译一段代码
contract Test {
bool a;
bool b;
uint8 f;
uint256 c;
bool d;
uint256 e;
function getA() public view returns (bool){
return a;
}
function setA(bool _a) public {
a = _a;
}
function getC() public view returns (uint256) {
return c;
}
function setC(uint256 _c) public {
c = _c;
}
function getD() public view returns (bool) {
return d;
}
function setD(bool _d) public {
d = _d;
}
}
在“Solidity Compile Details”中可以看到”STORAGELAYOUT”
{
"storage": [
{
"astId": 3,
"contract": "Test.sol:Test",
"label": "a",
"offset": 0,
"slot": "0",
"type": "t_bool"
},
{
"astId": 5,
"contract": "Test.sol:Test",
"label": "b",
"offset": 1,
"slot": "0",
"type": "t_bool"
},
{
"astId": 7,
"contract": "Test.sol:Test",
"label": "f",
"offset": 2,
"slot": "0",
"type": "t_uint8"
},
{
"astId": 9,
"contract": "Test.sol:Test",
"label": "c",
"offset": 0,
"slot": "1",
"type": "t_uint256"
},
{
"astId": 11,
"contract": "Test.sol:Test",
"label": "d",
"offset": 0,
"slot": "2",
"type": "t_bool"
},
{
"astId": 13,
"contract": "Test.sol:Test",
"label": "e",
"offset": 0,
"slot": "3",
"type": "t_uint256"
}
],
"types": {
"t_bool": {
"encoding": "inplace",
"label": "bool",
"numberOfBytes": "1"
},
"t_uint256": {
"encoding": "inplace",
"label": "uint256",
"numberOfBytes": "32"
},
"t_uint8": {
"encoding": "inplace",
"label": "uint8",
"numberOfBytes": "1"
}
}
}
我们可以看到a,b,f 所在slot的编号都是0,由此可以知道都被打包进了一个slot。但是d又是独占slot2。这里有个编译规则,如果是多个变量挤压在同一个slot中,则写的过程是SLOAD-SSTORE,如是变量独占一个slot,则写的过程就是SSTORE。前一个操作多了一个SLOAD,因此gas消费更多。
由此可以总结出
多个小变量打包进同一个 slot(如多个 bool、uint8、uint16),“单独写入”某个变量时,必须先 SLOAD slot(读出旧值)、在内存中修改对应 bit/byte、再 SSTORE(整体写回 slot)。
如果变量独占一个 slot(如 uint256、单独的 bool),写操作通常可以直接 SSTORE,不需要预读。
这个在初学的时候是非常容易混淆和不好理解的概念。
