WEB3DEV

Cover image for Solidity - Armazenamento vs. Memória vs. Dados de Chamada
Paulo Gio
Paulo Gio

Posted on • Atualizado em

Solidity - Armazenamento vs. Memória vs. Dados de Chamada

https://miro.medium.com/max/1100/1*ILxMCcLWMWQ4VMjstcC7wg.jpeg

Foto de Steve Johnson, no Unsplash

Sempre que estiver escrevendo contratos inteligentes no Solidity, você deve estar ciente de como suas variáveis e dados são tratados pela Máquina Virtual Ethereum (EVM). As escolhas que você fizer influenciarão, entre outras coisas, os custos de gás – para chamar suas funções ou implantar seu contrato – bem como o layout de armazenamento.

Dado que cada pedaço de espaço de bloco na Ethereum é altamente valorizado (daí o custo do ETH, embora tenha caído recentemente), isso influencia fortemente a eficiência do seu código e o contrato resultante, que esperamos que seja invocado com frequência. Cada chamada de função usa gás; cada pequena economia ajuda na soma do tempo de vida potencial do total de invocações de função.


Sumário

1 . Armazenamento

2 . Memória

3 . Dados de Chamada

4 . Comparações

4 . Referências


Armazenamento

Storage é o mais fácil de entender - é onde todas as variáveis de estado são armazenadas. Como o estado pode ser alterado em um contrato (por exemplo, dentro de uma função), as variáveis de armazenamento devem ser mutáveis. No entanto, sua localização é persistente e elas são armazenadas na blockchain.

As variáveis de estado em storage são organizadas de forma compacta — se possível, vários valores ocuparão o mesmo slot em storage. Além dos casos especiais de matrizes e structs dinamicamente dimensionados, outras variáveis são empacotadas em blocos de 32 bytes.

Se essas variáveis tiverem menos de 32 bytes, elas serão combinadas para ocupar o mesmo slot. Caso contrário, eles serão empurrados para o próximo slot de armazenamento. Os dados são armazenados de forma contígua (ou seja, um após o outro), a partir do slot 0 (até os slots 1, 2, 3, etc), por ordem de sua declaração no contrato.

Matrizes e structs dinâmicos sempre ocupam um novo slot, e quaisquer variáveis que os seguem também serão inicializadas para iniciar um novo slot de armazenamento.

Como o tamanho das matrizes e structs dinâmicos são desconhecidos a priori (ou seja, até que você os atribua posteriormente em seu contrato), eles não podem ser armazenados com seus dados entre outras variáveis de estado. Em vez disso, assume-se que eles ocupam 32 bytes e os elementos dentro deles são armazenados começando em um slot de armazenamento separado, que é calculado usando um hash Keccak-256.

No entanto, as variáveis de estado constante não são salvas em um slot de armazenamento. Em vez disso, elas são injetadas diretamente no bytecode do contrato — sempre que essas variáveis são lidas, o contrato as troca automaticamente pelo valor constante atribuído.

Memória

Memory é reservada para variáveis definidas no escopo de uma função. Elas só persistem enquanto uma função é chamada e, portanto, são variáveis temporárias que não podem ser acessadas fora desse escopo (ou seja, em qualquer outro lugar do seu contrato além dessa função). No entanto, elas são mutáveis dentro dessa função.

O Solidity reserva quatro slots de 32 bytes para a memória, com intervalos de bytes específicos, consistindo em: 1) espaço de rascunho de 64 bytes para métodos de hash; 2) 32 bytes para o tamanho da memória atualmente alocada, que é o ponteiro de memória livre onde o Solidity sempre coloca novos objetos; e 3) um slot zero de 32 bytes — que é usado como valor inicial para matrizes de memória dinâmica e nunca deve ser gravado.

Devido a essas diferenças de layout, há situações para matrizes e structs em que eles ocupam diferentes quantidades de espaço dependendo se estão em storage ou em memory.

Exemplo:

uint8[4] arr;

struct Str {
   uint v1;
   uint v2;
   uint8 v3;
   uint8 v4;
}
Enter fullscreen mode Exit fullscreen mode

Em ambos os casos, o array arr e o struct Str ocupam 128 bytes na memória (ou seja, 4 itens, 32 bytes cada). No entanto, como local de armazenamento, arr ocupa apenas 32 bytes (1 slot) enquanto Str ocupa 96 bytes (3 slots, 32 bytes cada).

Dados de Chamada

Calldata é um local imutável e temporário onde os argumentos da função são armazenados, se comportando principalmente como memory.

Recomenda-se tentar usar calldata porque evita cópias desnecessárias e garante que os dados fiquem inalterados. Matrizes e structs com localização de dados em calldata também podem ser retornados de funções.

Este tipo de dado é assumido em um formato definido pela especificação ABI, ou seja, preenchido em múltiplos de 32 bytes (o que difere das chamadas de funções internas). Os argumentos para construtores são um pouco diferentes, pois são anexados diretamente ao final do código do contrato (também na codificação ABI).

Comparações

Sempre que você definir uma variável de tipo de referência (matriz ou struct), também precisará definir sua localização de dados — a menos que seja uma variável de estado, caso em que é interpretada automaticamente como storage. Desde o Solidity v0.6.9, memory e calldata são permitidos em todas as funções independentemente do seu tipo de visibilidade (ou seja, externa, pública, etc).

As atribuições resultarão na criação de cópias ou meras referências ao mesmo dado - semelhante a objetos ou matrizes em Javascript:

  • Atribuições entre storage e memory (ou de calldata) sempre criam uma cópia separada.
  • Atribuições de memory para memory apenas criam referências. Portanto, alterar uma variável de memória altera todas as outras variáveis de memória que se referem aos mesmos dados.
  • As atribuições de storage para uma variável de armazenamento local também atribuem apenas uma referência.
  • Todas as outras atribuições à storage sempre são copiadas.

Para parâmetros de matrizes em funções, é recomendável usar calldata em vez de memory, pois isso proporciona uma economia significativa de gás. Por exemplo, uma função de soma que faz um loop em uma matriz de entrada pode economizar aproximadamente 1829 gás (~3,5%) usando calldata.

// Gás usado: 50992
function func1 (uint[] memory nums) external {
 for (uint i = 0; i < nums.length; ++i) {
    ...
 }
}// Gás usado: 49163
function func2 (uint[] calldata nums) external {
 for (uint i = 0; i < nums.length; ++i) {
    ...
 }
}
Enter fullscreen mode Exit fullscreen mode

E é isso! Nesse artigo tivemos algumas informações sobre locais de dados no Solidity. Obrigado por ler!

Referências

Artigo original escrito por YBM. Traduzido por Paulinho Giovannini.

Latest comments (0)