“Os programadores passam muito tempo se preocupando com a velocidade de seus programas, e as tentativas de obter eficiência geralmente têm um impacto negativo dramático na capacidade de depurar e dar suporte a eles. É necessário esquecer pequenas otimizações, por exemplo, em 97% dos casos. Otimização prematura é a raiz de todo mal! Mas não devemos perder de vista esses 3% onde é realmente importante! ”
Donald Knut.

Ao conduzir auditorias de contratos inteligentes, às vezes nos perguntamos se o desenvolvimento deles está relacionado aos 97% em que não há necessidade de pensar em otimização ou estamos lidando apenas com os 3% dos casos em que isso é importante. Na nossa opinião, provavelmente o segundo. Diferentemente de outros aplicativos, os contratos inteligentes não são atualizados, eles não podem ser otimizados "on the go" (desde que o algoritmo deles não seja estabelecido, mas esse é um tópico separado). O segundo argumento a favor da otimização
antecipada de contratos é que, ao contrário da maioria dos sistemas em que a subotimidade se manifesta apenas em escala, relacionada às especificidades de ferro e ambiente, é medida por um grande número de métricas, um contrato inteligente tem essencialmente a única métrica de desempenho - consumo de gás.
Portanto, é tecnicamente mais fácil avaliar a eficácia de um contrato, mas os desenvolvedores geralmente continuam confiando em sua intuição e fazem a mesma "otimização prematura" cega da qual o professor Knut falou. Verificaremos quão intuitiva a solução corresponde à realidade pelo exemplo da escolha da profundidade de bits de uma variável. Neste exemplo, como na maioria dos casos práticos, não obteremos economia e, vice-versa, nosso contrato se tornará mais caro em termos de gás consumido.
Que tipo de gás?
O Ethereum é como um computador global, cujo "processador" é a máquina virtual EVM, "código do programa" é uma sequência de comandos e dados gravados em um contrato inteligente, e chamadas são transações do mundo exterior. As transações são empacotadas em estruturas relacionadas - blocos que ocorrem uma vez a cada poucos segundos. E como o tamanho do bloco é limitado por definição e o protocolo de processamento é determinístico (requer processamento uniforme de todas as transações no bloco por todos os nós da rede), para satisfazer a demanda potencialmente ilimitada com um recurso limitado de nós e proteger contra o DoS, o sistema deve fornecer um algoritmo adequado para escolher a solicitação a ser atendida, e cujo ignorar, como mecanismo em muitas cadeias públicas de blockchain, existe um princípio simples - o remetente pode escolher a quantia de remuneração para o minerador por executar sua transação ktsii e escolhe mineiro cujas necessidades incluem um bloco, e não cuja, escolhendo o mais rentável para si próprios.
Por exemplo, no Bitcoin, onde o bloco é limitado a um megabyte, o minerador escolhe incluir a transação no bloco ou não com base no seu comprimento e na comissão proposta (escolhendo aqueles com a taxa máxima de satoshis por byte).
Para o protocolo Ethereum mais complexo, essa abordagem não é adequada, porque um único byte pode representar a ausência de uma operação (por exemplo, o código STOP) e a operação cara e lenta de gravação no armazenamento (SSTORE). Portanto, para cada código operacional no ar, seu próprio preço é fornecido, dependendo do consumo de recursos.
Tabela de taxas da especificação do protocolo
Tabela de fluxo de gás para diferentes tipos de operações. Na especificação do protocolo
Ethereum Yellow Paper .
Ao contrário do Bitcoin, o remetente da transação Ethereum não define a comissão em criptomoeda, mas a quantidade máxima de gás que ele está disposto a gastar -
startGas e o preço por unidade de gás -
gasPrice . Quando a máquina virtual executa o código, a quantidade de gás para cada operação subseqüente é subtraída de startGas até que a saída do código seja alcançada ou o gás acabe. Aparentemente, é por isso que um nome tão estranho é usado para esta unidade de trabalho - a transação é cheia de gasolina como um carro e chegará ao ponto de destino ou não, depende se há volume suficiente no tanque. Após a conclusão da execução do código para enviar um valor da transação é deduzido éster obtido multiplicando o gás efectivamente consumida por uma taxa de remetente predeterminados (
Wei gás por). Na rede global, isso acontece no momento da “mineração” do bloco, que inclui a transação correspondente, e no ambiente Remix, a transação é “minerada” instantaneamente, gratuitamente e sem quaisquer condições.
Nossa ferramenta - Remix IDE
Para o "perfil" do consumo de gás, usaremos o ambiente on-line para desenvolver os contratos Ethereum do
Remix IDE . Esse IDE contém editor de código de destaque de sintaxe, visualizador de artefatos, renderização da interface do contrato, depurador visual da máquina virtual, compiladores JS de todas as versões possíveis e muitas outras ferramentas importantes. Eu recomendo iniciar o estudo do éter com ele. Uma vantagem adicional é que ele não requer instalação - basta abri-lo em um navegador no
site oficial .
Seleção de tipo variável
A especificação da linguagem Solidity oferece ao desenvolvedor até 32 bits de tipos inteiros uint - de 8 a 256 bits. Imagine que você esteja desenvolvendo um contrato inteligente projetado para armazenar a idade de uma pessoa em anos. Que profundidade de bit você escolhe?
Seria bastante natural escolher o tipo mínimo suficiente para uma tarefa específica - o uint8 caberia matematicamente aqui. Seria lógico supor que quanto menor o objeto que armazenamos na blockchain e menos memória gastamos na execução, menos sobrecarga temos, menos pagamos. Mas na maioria dos casos, essa suposição estará incorreta.
Para o experimento, pegamos o contrato mais simples do que a
documentação oficial do
Solidity oferece e o coletamos em duas versões - usando o tipo variável uint256 e o tipo 32 vezes menor - uint8.
simpleStorage_uint256.sol pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
pragma solidity ^0.4.0; contract SimpleStorage { //uint is alias for uint256 uint storedData; function set(uint x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
simpleStorage_uint8.sol pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData; function set(uint8 x) public { storedData = x; } function get() public view returns (uint) { return storedData; } }
Medindo "economias"
Portanto, os contratos são criados, carregados no Remix, implantados e as chamadas para os métodos .set () são executadas pelas transações. O que nós vemos?
Gravar um tipo longo é mais caro que um curto - 20464 versus 20205 unidades de gás! Como Porque Vamos descobrir!

Loja uint8 vs uint256
Gravar no armazenamento persistente é uma das operações mais caras do protocolo por razões óbvias: primeiro, gravar o estado aumenta o tamanho do espaço em disco requerido pelo nó completo. O tamanho desse armazenamento está aumentando constantemente e, quanto mais estados são armazenados nos nós, mais lenta a sincronização ocorre, maior é o requisito de infraestrutura (tamanho da partição, número de Iops). Nos horários de pico, são as operações de E / S do disco lento que determinam o desempenho de toda a rede.
Seria lógico esperar que o armazenamento do uint8 custasse dezenas de vezes mais barato que o uint256. No entanto, no depurador, você pode ver que os dois valores estão localizados exatamente no mesmo slot de armazenamento que um valor de 256 bits.

E, neste caso específico, o uso do uint8 não oferece nenhuma vantagem no custo de gravação no armazenamento.
Manuseando uint8 vs uint256
Talvez tenhamos benefícios ao trabalhar com o uint8, se não durante o armazenamento, pelo menos ao manipular dados na memória? A seguir, compara as instruções para a mesma função obtida para diferentes tipos de variáveis.

Você pode ver que as operações com o uint8 têm ainda
mais instruções que o uint256. Isso ocorre porque a máquina converte o valor de 8 bits em uma palavra nativa de 256 bits e, como resultado, o código é cercado por instruções adicionais pagas pelo remetente. Não apenas escrever, mas também executar código com um tipo uint8, nesse caso, é mais caro.
Onde o uso de tipos curtos pode ser justificado?
Nossa equipe está envolvida na auditoria de contratos inteligentes há muito tempo e, até o momento, não houve um único caso prático em que o uso de um tipo pequeno no código fornecido para a auditoria levasse a economias. Enquanto isso, em alguns casos muito específicos, a economia é teoricamente possível. Por exemplo, se seu contrato armazena um grande número de pequenas variáveis ou estruturas de estado, elas podem ser compactadas em menos slots de armazenamento.
A diferença será mais aparente no exemplo a seguir:
1. contrato com 32 variáveis uint256
simpleStorage_32x_uint256.sol pragma solidity ^0.4.0; contract SimpleStorage { uint storedData1; uint storedData2; uint storedData3; uint storedData4; uint storedData5; uint storedData6; uint storedData7; uint storedData8; uint storedData9; uint storedData10; uint storedData11; uint storedData12; uint storedData13; uint storedData14; uint storedData15; uint storedData16; uint storedData17; uint storedData18; uint storedData19; uint storedData20; uint storedData21; uint storedData22; uint storedData23; uint storedData24; uint storedData25; uint storedData26; uint storedData27; uint storedData28; uint storedData29; uint storedData30; uint storedData31; uint storedData32; function set(uint x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } }
2. contrato com 32 variáveis uint8
simpleStorage_32x_uint8.sol pragma solidity ^0.4.0; contract SimpleStorage { uint8 storedData1; uint8 storedData2; uint8 storedData3; uint8 storedData4; uint8 storedData5; uint8 storedData6; uint8 storedData7; uint8 storedData8; uint8 storedData9; uint8 storedData10; uint8 storedData11; uint8 storedData12; uint8 storedData13; uint8 storedData14; uint8 storedData15; uint8 storedData16; uint8 storedData17; uint8 storedData18; uint8 storedData19; uint8 storedData20; uint8 storedData21; uint8 storedData22; uint8 storedData23; uint8 storedData24; uint8 storedData25; uint8 storedData26; uint8 storedData27; uint8 storedData28; uint8 storedData29; uint8 storedData30; uint8 storedData31; uint8 storedData32; function set(uint8 x) public { storedData1 = x; storedData2 = x; storedData3 = x; storedData4 = x; storedData5 = x; storedData6 = x; storedData7 = x; storedData8 = x; storedData9 = x; storedData10 = x; storedData11 = x; storedData12 = x; storedData13 = x; storedData14 = x; storedData15 = x; storedData16 = x; storedData17 = x; storedData18 = x; storedData19 = x; storedData20 = x; storedData21 = x; storedData22 = x; storedData23 = x; storedData24 = x; storedData25 = x; storedData26 = x; storedData27 = x; storedData28 = x; storedData29 = x; storedData30 = x; storedData31 = x; storedData32 = x; } function get() public view returns (uint) { return storedData1; } }
A implantação do primeiro contrato (32 uint256) custará menos - apenas 89941 de gás, mas .set () será muito mais caro, pois Ele ocupará 256 slots de armazenamento, o que custará 640.639 de gás para cada chamada. O segundo contrato (32 uint8) será duas vezes e meia mais caro ao implantar (gás 221663), mas cada chamada para o método .set () será muito mais barata, porque altera apenas uma célula do estágio (185291 gás).
Essa otimização deve ser aplicada?
Quão significativo é o efeito da otimização de tipo é um ponto discutível. Como você pode ver, mesmo para um caso sintético especialmente selecionado,
não obtivemos várias diferenças. A escolha de usar o uint8 ou o uint256 é mais uma ilustração do fato de que a otimização deve ser aplicada de maneira significativa (com entendimento de ferramentas, criação de perfil) ou não pensar sobre o assunto. Aqui estão algumas diretrizes gerais:
- se o contrato contiver muitos números pequenos ou estruturas compactas no repositório, você poderá pensar em otimização;
- se você usar o tipo "abreviado" - lembre -se das vulnerabilidades de excesso / insuficiência de fluxo ;
- para variáveis de memória e argumentos de função que não são gravados no repositório, é sempre melhor usar o tipo nativo uint256 (ou seu apelido uint). Por exemplo, não faz sentido definir o iterador da lista como uint8 - apenas perca;
- De grande importância para o empacotamento correto nos slots de armazenamento para o compilador é a ordem das variáveis no contrato .
Referências
Termino com conselhos que não têm contra-indicações: experimente ferramentas de desenvolvimento, conheça as especificações da linguagem, biblioteca e estruturas. Aqui estão os links mais úteis, na minha opinião, para começar a aprender sobre a plataforma Ethereum:
- contratos de ambiente de desenvolvimento Remix - IDE muito funcional baseada em browser;
- Na especificação da linguagem Solidity , o link irá especificamente para a seção Layout de Variáveis de Estado;
- Um repositório de contratos muito interessante da famosa equipe do OpenZeppelin. Exemplos da implementação de tokens, contratos de crowdsale e, o mais importante - a biblioteca SafeMath , que ajuda a trabalhar com segurança com tipos;
- Ethereum Yellow Paper , especificação formal da máquina virtual Ethereum;
- Ethereum White Paper , a especificação da plataforma Ethereum, um documento mais geral e de alto nível com um grande número de links;
- Ethereum em 25 minutos , uma breve introdução técnica ao Ethereum, curta, mas no entanto abrangente, do criador da plataforma, Vitalik Buterin;
- Etherscan blockchain explorer , uma janela para o mundo real do éter, um navegador de blocos, transações, tokens, contratos na rede principal. No Etherscan, você encontrará o explorador para redes de teste Rinkeby, Ropsten, Kovan (redes com transmissão gratuita, baseadas em diferentes protocolos de consenso).