Abaixo está uma lista com algumas maneiras simples de economizar gás em contratos do Solidity. A lista completa parece interminável, mas tudo se resume a encontrar a maneira mais eficiente de se realizar uma ação com o número mínimo de operações.
Índice
- Dados de estado vs dados de bytecode
- O poder dos loops do-while vs loops for
- i++ vs ++i
- Ignorar matemática segura — unchecked{}
- Aderindo aos tipos de dados preferenciais
- Calldata vs Memory
- Utilize a memória para armazenar variáveis de leitura em cache
- Gerenciamento eficiente de variáveis
- Economize gás na primeira gravação
- Chamando dados de struct
1. Dados de estado vs dados de bytecode
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
uint256 public _a = 10;
// 23311 gás
function getValue() public returns (uint256){
return _a;
}
}
contract SaveGas2 {
uint256 public constant _b = 10;
// 21211 gás
function getValue() public returns (uint256){
return _b;
}
}
contract SaveGas3 {
// 21210 gás
function getValue() public returns (uint256){
return 1000;
}
}
Ao armazenar _b
como uma constante, ele é armazenado como dados imutáveis no bytecode do contrato. O mesmo vale para o nosso número mágico 1000
. _a
por outro lado é armazenado no estado do contrato e requer uma operação SLOAD
para lê-lo.
2. O poder dos loops do-while vs loops for
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
// 43406 gás
function loop() public {
for (uint256 i; i < 200; i++) {}
}
}
contract SaveGas2 {
// 40579 gás
function loop() public {
uint256 i;
do {
i++;
} while (i < 200);
}
}
Os loops são bem comuns em quase todas as linguagens de codificação e essa familiaridade pode nos tornar complacentes.
O loop padrão for
verificará a condição antes de executar a instrução. Um do while
executará a instrução pelo menos uma vez antes e, em seguida, verificará as condições, permitindo economia de gás.
Aviso: use apenas um do-while quando tiver certeza de que o código deve ser executado pelo menos uma vez e lembre-se de lidar com suas condições para evitar um loop infinito e escape de gás.
Para obter informações sobre como os loops do-while diferem dos loops while, confira este artigo:
https://www.guru99.com/while-vs-do-while.html
3. i++ vs ++i
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
// 43406 gás - i++
// 42406 gás - ++i
function loop() public {
for (uint256 i; i < 200; ++i) {}
}
}
contract SaveGas2 {
// 40579 gás - i++
// 39579 gás - ++i
function loop() public {
uint256 i;
do {
++i;
} while (i < 200);
}
}
Eu sei, eu sei, é um pouco louco, mas os números não mentem - mas por que é mais barato?
i++
incrementa o valor de i
armazenando o valor original na memória, incrementando-o, então armazenando o valor resultante na memória temporária e retornando o valor original. Após esse retorno, o novo valor de i
é atualizado. Ao todo, são necessárias 4 operações.
++i
incrementa o valor de i e retorna esse valor, o que leva 2 operações.
4. Ignorar matemática segura — unchecked{}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
// 43637 gás - i++
// 42637 gás - ++i
// 29806 gás - unchecked
function loop() public {
for (uint256 i; i < 200;) {
unchecked{++i;}
}
}
}
contract SaveGas2 {
// 40832 gás - i++
// 39832 gás - ++i
// 26979 gás - unchecked
function loop() public {
uint256 i;
do {
unchecked{++i;}
} while (i < 200);
}
}
Dos documentos do Solidity:
https://docs.soliditylang.org/en/v0.8.0/control-structures.html#checked-or-unchecked-arithmetic
Antes do Solidity 0.8.0, as operações aritméticas sempre se encaixavam em caso de underflow ou overflow, levando ao uso generalizado de bibliotecas que introduziam verificações adicionais.
Desde o Solidity 0.8.0, todas as operações aritméticas revertem em overflow e underflow por padrão, tornando desnecessário o uso dessas bibliotecas.
Para obter o comportamento anterior, um bloco unchecked pode ser usado:
O que significa que você pode economizar gás empregando o bloco unchecked
para evitar verificações desnecessárias em seu ++i
.
No código de exemplo acima, é seguro fazer isso porque sabemos que ++i
não causará o overflow de 2256, ambos os loops pararão de processar quando i == 200
.
5. Aderindo aos tipos de dados preferenciais
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
// 28179 gás
function loop() public {
uint16 i;
do {
unchecked{++i;}
} while (i < 200);
}
}
contract SaveGas2 {
// 26979 gás
function loop() public {
uint256 i;
do {
unchecked{++i;}
} while (i < 200);
}
}
A EVM (Ethereum Virtual Machine) é controlado por variáveis de 256 bits (32 bytes). Isso significa que em nosso loop acima, a variável uint16
é convertida em um uint256
antes de ser usada, e essa conversão custa gás.
Como você pode ver, economizamos gás usando uint256
para a variável i
.
6. Calldata vs Memory
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
// x = [1...10]
// 24427 gás
function loop(uint256[] memory x) public {
uint256 i;
do {
unchecked{++i;}
} while (i < x.length);
}
}
contract SaveGas2 {
// x = [1...10]
// 23426 gás
function loop(uint256[] calldata x) public {
uint256 i;
do {
unchecked{++i;}
} while (i < x.length);
}
}
Dos documentos do Solidity:
https://docs.soliditylang.org/en/v0.5.11/types.html?highlight=memory#data-location
Calldata (dados de chamada) só é válido para parâmetros de funções de contrato externas e é necessário para este tipo de parâmetro. Calldata é uma área não modificável e não persistente onde os argumentos da função são armazenados e se comporta principalmente como memory (memória).
Sugere-se tentar usar calldata sempre que possível, pois evita cópias desnecessárias, daí o preço mais baixo de gás, e também garante que os dados sejam imutáveis.
Vale a pena notar que, desde o Solidity 0.6.9, o uso de memory
e calldata
não é limitado pela visibilidade da função.
Para mais detalhes sobre as diferenças entre storage
(armazenamento), memory
e calldata
, confira este artigo:
https://www.web3dev.com.br/paulogio/solidity-armazenamento-vs-memoria-vs-dados-de-chamada-23o4
7. Utilize a memória para armazenar variáveis de leitura em cache
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
uint256 _data = 1;
// 25319 gás
function loop() public {
uint256 i;
do {
uint256 solution = i + _data;
unchecked{++i;}
} while (i < 10);
}
}
contract SaveGas2 {
uint256 _data = 1;
// 24484 gás
function loop() public {
uint256 i;
uint256 data = _data;
do {
uint256 solution = i + data;
unchecked{++i;}
} while (i < 10);
}
}
Acessar uma variável de armazenamento “frio” (cold storage) - a primeira leitura - custa 2100 gás
, e após, 100 gás
- acesso ao armazenamento “morno” (warm storage)
Você pode economizar gás armazenando em cache a variável de armazenamento e lendo os dados armazenados em cache em vez dos dados de armazenamento. Isso também funciona ao ler dados de uma matriz de armazenamento.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
uint256[] public _arr = [1,2,3,4,5,6,7,8,9,10];
// 46506 gás
function loop() public {
uint256 i;
uint256 total;
do {
unchecked{
total += _arr[i];
++ i;
}
} while (i < 10);
}
}
contract SaveGas2 {
uint256[] public _arr = [1,2,3,4,5,6,7,8,9,10];
// 46130 gás
function loop() public {
uint256 i;
uint256 total;
uint256[] memory arr = _arr;
do {
unchecked{
total += arr[i];
++ i;
}
} while (i < 10);
}
}
No exemplo acima estamos armazenando _arr
na memória de SaveGas2 > loop()
. Todas as leituras são então chamadas de nosso novo arr
.
Este é um ótimo método de economia de gás, desde que a matriz de armazenamento não seja muito grande. Os dados precisam ser copiados para a memória, portanto matrizes grandes serão ineficientes.
8. Gerenciamento eficiente de variáveis
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
uint256 _data = 1;
// 24484 gás
function loop() public {
uint256 i;
uint256 data = _data;
do {
uint256 solution = i + data;
unchecked{++i;}
} while (i < 10);
}
}
contract SaveGas2 {
uint256 _data = 1;
// 24459 gás
function loop() public {
uint256 i;
uint256 data = _data;
uint256 solution;
do {
solution = i + data;
unchecked{
++i;
}
} while (i < 10);
}
}
contract SaveGas3 {
uint256 _data = 1;
// 23719 gás
function loop() public {
uint256 i;
uint256 data = _data;
uint256 solution;
do {
unchecked{
solution = i + data;
++i;
}
} while (i < 10);
}
}
Se você planeja reutilizar uma variável como a variável solution
e sabe que ela não herdará o valor do loop anterior, você pode economizar gás definindo a variável fora do loop — compare os loops em SaveGas
e SaveGas2
.
Além disso, se você souber que não há risco de overflow, poderá economizar mais gás aninhando o cálculo de solution
no bloco unchecked
.
9. Economize gás na primeira gravação
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
uint256 _data;
// #1: 43494 gás
// #n: 23594 gás
function setData(uint256 x) public {
_data = x;
}
}
contract SaveGas2 {
uint256 _data = 1;
// #1: 23594 gás
// #n: 23594 gás
function setData(uint256 x) public {
_data = x;
}
}
Alterar um valor de armazenamento zero para um diferente de zero custará 20000 gás
e todas as gravações diferentes de zero custarão 5000 gás
. Ao primeiro usuário que chamar a função, podemos economizar algum gás definindo um valor diferente de zero para _data
.
10. Chamando dados de struct
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
contract SaveGas {
struct MyData {
uint128 x;
uint128 y;
uint256 z;
}
mapping(address => MyData) _userData;
// x: 1
// y: 1
// z: 1
function setUser(
uint128 x,
uint128 y,
uint256 z
) external {
_userData[msg.sender] = MyData(x, y, z);
}
// 25615 gás
function getUserZ() public returns (uint256) {
MyData memory mydata = _userData[msg.sender];
return mydata.z;
}
}
contract SaveGas2 {
struct MyData {
uint128 x;
uint128 y;
uint256 z;
}
mapping(address => MyData) _userData;
// x: 1
// y: 1
// z: 1
function setUser(
uint128 x,
uint128 y,
uint256 z
) external {
_userData[msg.sender] = MyData(x, y, z);
}
// 23387 gás
function getUserZ() public returns (uint256) {
return _userData[msg.sender].z;
}
}
A EVM armazena dados sequencialmente, tentando empacotar tudo em slots de 256 bits (32 bytes). No exemplo acima, os dados de struct são organizados de modo que x
e y
ocupem um único espaço de memória de 256 bits e z
ocupe seu próprio espaço - confortável e organizado.
Quando se trata de ler nossos valores, podemos economizar gás minimizando quantos espaços de memória temos que ler e/ou armazenar em cache.
SaveGas > getUserZ()
está armazenando a struct MyData
relevante na memória — fazendo uma cópia de ambos os espaços de memória e lendo os dados relevantes.
SaveGas2 > getUserZ()
está lendo dados de um único espaço de memória contendo z
.
Artigo original escrito por xtremetom. Traduzido por Paulinho Giovannini.
Latest comments (0)