• 以太坊开发技巧:Solidity 中利用位图大幅节省Gas费
  • 发布于 2个月前
  • 465 热度
    0 评论
有过合约开发经验的同学都可能知道的,以太坊中最昂贵的操作是存储数据(SSTORE[4])。所以大家也应该一直寻找方法来减少存储需求。让我们来探讨一个特别有用的方法:位图
注:在 Uniswap 的代码中,有很多使用位图来优化 gas 的技巧。

如何实现一个简单的位图
假设我们想存储 10 个布尔值。通常,我们会用一个简单的布尔数组来实现这一点,例如:
// 堆代码 www.duidaima.com
// 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 位,我们可以使用位运算[5]。

让我们来看看 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 成本差异。用原来的布尔数组,交易的执行成本是:
1. setDataWithBoolArray: 140,583 gas
2. ReadWithBoolArray: 1,281 gas
现在有了位图,我们可以大大改善这个情况:
1.setDataWithBitmap: 78,043 gas
r2.eadWithBitmap: 1,129 gas
使用场景 1:设置布尔开关
现在来看看第一个使用场景: 布尔开关[6]通常被用来激活系统中的某些选项。比方说,你建立了一个像 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;
    }
}


用户评论