尽量使用 256 位的变量,例如 uint256 和 bytes32!乍一看,这似乎有点违反直觉,但是当你更仔细地考虑以太坊虚拟机(EVM)的运行方式时,这完全有意义。每个存储插槽都有 256 位。因此,如果你只存储一个 uint8,则 EVM 将用零填充所有缺少的数字,这会耗费 gas。此外,EVM 执行计算也会转化为 uint256 ,因此除 uint256 之外的任何其他类型也必须进行转换。
注意:通常,应该调整变量的大小,以便填满整个存储插槽。
一种相对便宜的存储和读取信息的方法是,将信息部署在区块链上时,直接将其包含在智能合约的字节码中。不利之处是此值以后不能更改。但是,用于加载和存储数据的 gas 消耗将大大减少。有两种可能的实现方法:
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.11;
-
- contract CryptosTribeToken {
- uint256 add;
- uint256 public v1;
- uint256 public immutable v2=10;
-
- function calculate() public view returns (uint256 result) {
- return v1 * v2 * 10000;
- }
- }
当你将数据永久存储在区块链上时,要在后台执行汇编命令 SSTORE。这是最昂贵的命令,费用为 20,000 gas,因此我们应尽量少使用它。在内部结构体中,可以通过简单地重新排列变量来减少执行的 SSTORE 操作量,如以下示例所示:
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.11;
-
- contract CryptosTribeToken {
- uint256 add;
- uint256 public v1;
- uint256 public immutable v2=10;
-
- function calculate() public view returns (uint256 result) {
- return v1 * v2 * 10000;
- }
- struct Data {
- uint64 a;
- uint64 b;
- uint128 c;
- uint256 d;
- }
- Data public data;
- constructor(uint64 _a, uint64 _b, uint128 _c, uint256 _d) {
- data.a = _a;
- data.b = _b;
- data.c = _c;
- data.d = _d;
- }
- }
请注意,在 struct 中,所有可以填充为 256 位插槽的变量都彼此相邻排序,以便编译器以后可以将它们堆叠在一起(也使用占用少于 256 位的那些变量)。在上面的例子中,仅使用两次 STORE 操作码,一次用于存储a,b和c,另一次用于存储d。这同样适用于在结构体外部的变量。另外,请记住,将多个变量放入同一个插槽所节省的费用要比填满整个插槽(首选数据类型)所节省的费用大得多。
如果将结构体Data中c和d位置调换,那么将使用3次store,a和b一次,c一次,d一次,gas就会币之前多
也可以手动应用将变量堆叠在一起以减少执行的 SSTORE 操作的技术。下面的代码将 4 个 uint64 类型的变量堆叠到一个 256 位插槽中。
编码:将变量合并为一个。
注意:请记得使用编译器打包优化
- function encode(uint64 _a, uint64 _b, uint64 _c, uint64 _d) internal pure returns (bytes32 x) {
- assembly {
- let y := 0
- mstore(0x20, _d)
- mstore(0x18, _c)
- mstore(0x10, _b)
- mstore(0x8, _a)
- x := mload(0x20)
- }
- }
为了读取,将需要对该变量进行解码,这可以通过第二个功能实现。
解码:将变量拆分为其初始部分。
- function decode(bytes32 x) internal pure returns (uint64 a, uint64 b, uint64 c, uint64 d) {
- assembly {
- d := x
- mstore(0x18, x)
- a := mload(0)
- mstore(0x10, x)
- b := mload(0)
- mstore(0x8, x)
- c := mload(0)
- }
- }
将这种方法的 gas 消耗量与上述方法的 gas 消耗量进行比较,你会注意到,由于多种原因,这种方法的成本明显降低:
那么,为什么还要使用以前的呢?从这两种实现来看,很明显,我们使用汇编来解码变量,就放弃了代码的可读性,因此,使第二种方法更容易出错。另外,由于每种情况下我们都必须包含编码和解码函数,因此部署成本也将大大增加。但是,如果你确实需要降低函数的 gas 消耗, (与其他方法相比,装入单个插槽中的变量越多,节省的费用就越高。)