WEB3DEV

Cover image for 10 Maneiras Simples de Economizar Gás com o Solidity
Paulo Gio
Paulo Gio

Posted on • Atualizado em

10 Maneiras Simples de Economizar Gás com o Solidity

https://miro.medium.com/max/750/1*OXQt-wvivqLIwGxunNh5pg.jpeg

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

  1. Dados de estado vs dados de bytecode
  2. O poder dos loops do-while vs loops for
  3. i++ vs ++i
  4. Ignorar matemática segura — unchecked{}
  5. Aderindo aos tipos de dados preferenciais
  6. Calldata vs Memory
  7. Utilize a memória para armazenar variáveis de leitura em cache
  8. Gerenciamento eficiente de variáveis
  9. Economize gás na primeira gravação
  10. 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

Oldest comments (0)