有过合约开发经验的同学都可能知道的,以太坊中最昂贵的操作是存储数据(SSTORE)。所以大家也应该一直寻找方法来减少存储需求。让我们来探讨一个特别有用的方法:位图
注:在 Uniswap 的代码中,有很多使用位图来优化 gas 的技巧。
如何实现一个简单的位图
假设我们想存储10个布尔值。通常,我们会用一个简单的布尔数组来实现这一点,例如:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract BitmapTest {
bool[10] implementationWithBool;
function setDataWithBoolArray(bool[10] memory data) external {
implementationWithBool = data;
}
function readWithBoolArray(uint256 index) external returns (bool) {
return implementationWithBool[index];
}
}
而使用 Bitmap 位图,可以用一个 uint10代替bool数组来实现。uint10将在存储中用10 位(bits 比特位)表示。
例如,这里有一些用比特(bit)表示的十进制数字:
- 0: 0000000000
- 1: 0000000001
- 512: 0100000000
- 729: 1011011001
- 1023: 1111111111
我们可以用一些额外的数学方法来利用这种位表示法。为了得到这个整数的第n位,我们可以使用位运算。
让我们来看看729这个数字,在常规方式下,用一个bool数组来读取第4个bool值,它只是一个array[4]。对于位图,我们可以通过使用左移运算符<<将1向左移,来代替创建第二个数字。
1 << 4 = 0000000001 << 4 = 0000010000
现在使用位和运算符&,我们可以得到第n位的值(从0开始计算)。
729 & (1 << 4) = 1011011001 & 0000010000
其结果是
- 1011011001 &
- 0000010000 =
- 0000010000
只要这个结果 大于0 ,原数的第n位就是1,所以现在我们可以实现位图:
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract BitmapTest {
uint256 implementationWithBitmap;
function setDataWithBitmap(uint256 data) external {
implementationWithBitmap = data;
}
function readWithBitmap(uint256 indexFromRight) external returns (bool) {
uint256 bitAtIndex = implementationWithBitmap & (1 << indexFromRight);
return bitAtIndex > 0;
}
}
选择位图大小
你可能已经注意到,我们在上面的实现中选择了uint256。虽然uint10在技术上是足够的,但这实际上会导致比使用uint256更高的Gas成本。这是因为EVM在32个字节的寄存器(256位)上操作,任何低于这个数字的都需要额外的转换。
所以你应该总是选择 uint256 吗?
也不是,这取决于你的使用情况。用一个uint256,你可以表示256位。那么你想存储的数据是否适合一个256位的布尔数组?如果是,那么就继续使用单个uint256。
如果不能,例如布尔数组可以任意增长,那么就把位图本身打包成一个数组。我将在最后用一个例子来探讨这两种选择。
比较Gas成本
让我们先来看看10位例子中的Gas成本差异。用原来的布尔数组,交易的执行成本是:
- setDataWithBoolArray: 140,583 gas
- ReadWithBoolArray: 1,281 gas
现在有了位图,我们可以大大改善这个情况:
- setDataWithBitmap: 78,043 gas
- readWithBitmap: 1,129 gas
使用场景1:设置布尔开关
现在来看看第一个使用场景: 布尔开关通常被用来激活系统中的某些选项。
比方说,你建立了一个像Uniswap一样的DEX,你可以自动触发的交易。你可以根据交易的来源来激活某些设置。例如,你可能有如下开关
- NO_FEES (无交易费)
- ...
- SENDING_FEES_TO_GOVERNANCE (发送费用到治理)
- DELAY_TRADE_EXECUTION (延迟交易执行)
这些选项可能不会超过256个,所以你可以很容易地将这些选项存储在一个uint256中。
使用场景2:参与者的名单
你可能想向任何参与过你的合约的人支付奖励。这可能是一个任意的大列表。你可以在一个映射中保存每个参与者,或者用一个uint256数组来代替位图。
// SPDX-License-Identifier: MIT
pragma solidity 0.8.0;
contract ParticipatedWithBitmap {
uint256[] public participantsBitmap;
function setParticipants(uint256[] memory participantsBitmap_) external onlyOwner {
participantsBitmap = participantsBitmap_;
}
function hasParticipated(uint256 bitmapIndex, uint256 indexFromRight) external view returns (bool) {
uint256 bitAtIndex = participantsBitmap[bitmapIndex] & (1 << indexFromRight);
return bitAtIndex > 0;
}
}
欢迎订阅专栏 学习更多 Solidity 高阶优化技巧。
转载:https://learnblockchain.cn/article/5287
版权属于:区块链中文技术社区 / 转载原创者
本文链接:https://bcskill.com/index.php/archives/1529.html
相关技术文章仅限于相关区块链底层技术研究,禁止用于非法用途,后果自负!本站严格遵守一切相关法律政策!