有过合约开发经验的同学都可能知道的,以太坊中最昂贵的操作是存储数据(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