[译]Gas 优化 - 如何优化存储

  • Tiny熊
  • 更新于 2020-09-28 11:08
  • 阅读 5021

大多数dApp和游戏都需要将数据存储在区块链上,因此必须与存储进行交互。 优化智能合约的gas成本是一项重要的工作。

我们会很快进入一些非常技术性的Solidity概念。

变量合并

Solidity(用于以太坊智能合约的编程语言)中,你拥有“内存(memory)”(想像计算机上的RAM)和“存储(storage)”(想像硬盘驱动器)。 两者均以32字节的块为操作单位(一个字节大约是一个字母)。 在Solidity 中,内存价格便宜(存储或更新值仅需要 3 gas)。 存储很昂贵(存储新的值需要20,000 gas,更新值需要 5000 gas)。

大多数dApp和游戏都需要将数据存储在区块链上,因此必须与存储进行交互。 优化智能合约的gas成本是一项重要的工作。

这是一个简单的区块链游戏可能存储的数据:

address owner;
uint64 creationTime;
uint256 dna;
uint16 strength;
uint16 race;
uint16 class;

如果我们只是直接存储这些值,则可能需要执行以下操作:

mapping(uint256 => address) owners;
mapping(uint256 => uint64) creationTime;
mapping(uint256 => uint256) dna;
mapping(uint256 => uint16) strength;
mapping(uint256 => uint16) race;
mapping(uint256 => uint16) class;

存储这些数据需要花费120,000 gas(12万),这确实非常昂贵! 当我们构造一个结构体并将其存储时,我们会得到更好的结果:

struct GameCharacter {
  address owner;
  uint64 creationTime;
  uint256 dna;
  uint16 strength;
  uint16 race;
  uint16 class;
}

mapping(uint256 => GameCharacter) characters;

现在,当我们存储此结构体时,我们支付的费用更少:75000 gas 。 编译器会将 owner 和creationTime 巧妙地打包到同一插槽中,因此花费25,000,而不是40,000 (译者注,因为第2个写值被当做更新)。 同样效果却更少的花费,不过,我们可以做得更好:

struct GameCharacter {
  address owner;
  uint48 creationTime;
  uint16 strength;
  uint16 race;
  uint16 class;
  uint256 dna;
}
mapping(uint256 => GameCharacter) characters;

新代码的花费:60,000 gas! 我们在此处进行了两项更改:首先,将dna移至末尾。 它是一个uint256,已经是32个字节,因此不能包含任何其他的内容。 然后,我们将 creationTime 的类型从uint64 更改为uint48。 这使前五个字段全部打包成32个字节。 时间戳记不必超过uint48 类,而且Solidity(与其他大多数语言不同)允许uint为8的任意倍数,而不仅仅是8/16/32/64等(如果你迫切地需要空间, 你可以将时间戳使用uint32,在2106年它可能导致问题之前,你可能已经死了 :) 。

我们将费用减半了 -- 很好,对吧? 好吧,不 -- 我们希望所有功能的 gas 消耗都尽可能小,并且仍可以通过将前5个数据字段编码为单个 uint256 来降低成本:

mapping(uint256 => uint256) characters;
mapping(uint256 => uint256) dnaRecords;
function setCharacter(uint256 _id, address owner, uint256 creationTime, uint256 strength, uint256 race, uint256 class, uint256 dna) 
    external 
{
    uint256 character = uint256(owner);
    character |= creationTime<<160;
    character |= strength<<208;
    character |= race<<224;
    character |= class<<240;
    characters[_id] = character;
    dnaRecords[_id] = dna;
}

function getCharacter(uint256 _id) 
    external view
returns(address owner, uint256 creationTime, uint256 strength, uint256 race, uint256 class, uint256 dna) {
    uint256 character = characters[_id];
    dna = dnaRecords[_id];
    owner = address(character);
    creationTime = uint256(uint40(character>>160));
    strength = uint256(uint16(character>>208));
    race = uint256(uint16(character>>224));
    class = uint256(uint16(character>>240));
}

将数据存储为 uint256 只会花费40,000多gas -- 仅进行两次存储操作,再加上一些移位和按位或运算。 考虑到我们最初为120,000gas,这是一个很大的进步! 使用此方法检索数据也要便宜一些。 请注意,这两个函数不会执行任何错误检查-你需要自己执行此操作,以确保所有输入的最终值都不会超过其最大值(但你必须在所有这些实现中进行相同的检查)。

编码与解码

上面的代码中可能有一些字符让你感觉陌生。 让我们来一探究竟:

|=

这是按位或赋值运算符。 用来组合两个二进制值(我们在计算机上,所以一切都是二进制的),方法是“如果其中任一位为1,则结果中的该位为1”。 我们可以在这里使用它,因为我们以uint256(address)开头,这意味着我们知道在位160之上的所有位均为0。

<<

这是左移位运算符。 它取一个数字的位,然后将它们向左移动。 因此,creationTime<<160 并将creationTime移至结果代码中的插槽160–207中。 将移位和按位或赋值运算相结合,就可以构建编码。

>>

这是右移位运算符。 它的工作方式与<<类似,但方向相反。 我们可以使用此方法从编码数据转换回来。 但是,我们还需要:

uint256(uint48())

这利用了solidity编译器的功能。 如果将uint256转换为uint48,则会丢弃所有高于位 48 的位。 这对于我们的目的而言是完美的,因为我们知道 creationTime 的长度为48位,因此仅提取所需的数据。

使用这些技术,我们可以显着提高智能合约的性能。

使用数据

现在你已经有了数据存储,你可能需要在函数之间传递数据。 除非你的应用程序像这里描述的那样简单,否则你将遇到16个局部变量的堆栈限制。 因此,你需要将数据作为结构体传递到内存中。 此结构体看起来与之前显示的结构略有不同:

struct GameCharacter {
  address owner;
  uint256 creationTime;
  uint256 strength;
  uint256 race;
  uint256 class;
  uint256 dna;
}

function getCharacterStruct(uint256 _id) 
    external view
returns(GameCharacter memory _character) {
    uint256 character = characters[_id];
    _character.dna = dnaRecords[_id];
    _character.owner = address(character);
    _character.creationTime = uint256(uint40(character>>160));
    _character.strength = uint256(uint16(character>>208));
    _character.race = uint256(uint16(character>>224));
    _character.class = uint256(uint16(character>>240));
}

所有字段均为uint256。 这是 solidity 中最有效的数据类型。 内存中的变量(甚至是结构体)根本没有打包,因此在内存中使用uint16不会获得任何好处,而且由于solidity必须执行额外的操作才能将uint16转换为uint256进行计算,所以你也许会迷失方向。

总结

我们确实在1980年代早期编写了一个兔子洞编程-对数据进行编码,需要关注我们可以从代码中抽出的每一个小优化。 编写Solidity不同于编写现代语言,这是一种不同的”物种" - 以太坊区块链的限制意味着你正在有效地为功能不如1973 Apple 1 。

每一点细微的优化都会帮助你实现更有效的存储方法, 来为你和你的用户节省一些gas。


本翻译由 Cell Network 赞助支持。

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Tiny熊
Tiny熊
0xD682...E8AB
登链社区发起人 通过区块链技术让世界变得更好而尽一份力。