“Los programadores pasan una gran cantidad de tiempo preocupándose por la velocidad de sus programas, y los intentos por lograr la eficiencia a menudo tienen un impacto negativo dramático en la capacidad de depurarlos y respaldarlos. Es necesario olvidarse de las pequeñas optimizaciones, por ejemplo, en el 97% de los casos. ¡La optimización prematura es la raíz de todo mal! ¡Pero no debemos perder de vista el 3% donde es realmente importante!
Donald Knut

Cuando realizamos auditorías de contratos inteligentes, a veces nos preguntamos si su desarrollo se relaciona con el 97% donde no hay necesidad de pensar en la optimización o si estamos tratando solo con el 3% de los casos en los que es importante. En nuestra opinión, lo más probable es el segundo. A diferencia de otras aplicaciones, los contratos inteligentes no se actualizan, no pueden optimizarse "sobre la marcha" (siempre que su algoritmo no esté establecido, pero este es un tema aparte). El segundo argumento a favor de
la optimización
temprana del contrato es que, a diferencia de la mayoría de los sistemas donde la suboptimidad se manifiesta solo en escala, relacionada con los detalles específicos del hierro y el medio ambiente, se mide por una gran cantidad de métricas, un contrato inteligente tiene esencialmente la única métrica de rendimiento: consumo de gas.
Por lo tanto, es técnicamente más fácil evaluar la efectividad de un contrato, pero los desarrolladores a menudo continúan confiando en su intuición y hacen la misma "optimización prematura" ciega de la que habló el profesor Knut. Comprobaremos qué tan intuitiva corresponde la solución a la realidad mediante el ejemplo de elegir la profundidad de bits de una variable. En este ejemplo, como en la mayoría de los casos prácticos, no lograremos ahorros, e incluso viceversa, nuestro contrato resultará más costoso en términos de consumo de gas.
¿Qué tipo de gas?
Ethereum es como una computadora global, cuyo "procesador" es la máquina virtual EVM, el "código de programa" es una secuencia de comandos y datos grabados en un contrato inteligente, y las llamadas son transacciones que provienen del mundo exterior. Las transacciones se empaquetan en estructuras relacionadas, bloques que ocurren una vez cada pocos segundos. Y dado que el tamaño del bloque es, por definición, limitado, y el protocolo de procesamiento es determinista (requiere un procesamiento uniforme de todas las transacciones en el bloque por parte de todos los nodos de la red), para satisfacer una demanda potencialmente ilimitada con un recurso limitado de nodos y proteger contra DoS, el sistema debe proporcionar un algoritmo justo para elegir la solicitud de quién atender, y cuya ignorancia, como mecanismo en muchas cadenas de bloques públicas, existe un principio simple: el remitente puede elegir la cantidad de remuneración para el minero por realizar su transferencia ktsii y elige minero que necesiten disponer de un bloque, y no cuyos, la elección de los más rentables por sí mismos.
Por ejemplo, en Bitcoin, donde el bloque está limitado a un megabyte, el minero elige incluir la transacción en el bloque o no en función de su longitud y la comisión propuesta (elegir aquellos con la relación máxima de satoshis por byte).
Para el protocolo Ethereum más complejo, este enfoque no es adecuado, porque un solo byte puede representar tanto la ausencia de una operación (por ejemplo, el código STOP) como la operación de escritura lenta y costosa en el almacenamiento (SSTORE). Por lo tanto, para cada código de operación en el aire se proporciona su propio precio, dependiendo de su consumo de recursos.
Lista de tarifas de la especificación del protocolo
Tabla de flujo de gas para diferentes tipos de operaciones. De la especificación del protocolo
Ethereum Yellow Paper .
A diferencia de Bitcoin, el remitente de la transacción Ethereum no establece la comisión en criptomoneda, sino la cantidad máxima de gas que está dispuesto a gastar:
startGas y el precio por unidad de gas:
gasPrice . Cuando la máquina virtual ejecuta el código, la cantidad de gas para cada operación posterior se resta de startGas hasta que se alcanza la salida del código o se agota el gas. Aparentemente, es por eso que se usa un nombre tan extraño para esta unidad de trabajo: la transacción se llena con gas como un automóvil, y llegará al punto de destino o no, dependiendo de si hay suficiente volumen lleno en el tanque. Al finalizar la ejecución del código, la cantidad de aire recibida al multiplicar el gas realmente consumido por el precio establecido por el remitente (
wei por gas) se debita del remitente de la transacción. En la red global, esto sucede en el momento de "extraer" el bloque, que incluye la transacción correspondiente, y en el entorno Remix, la transacción se "extrae" al instante, de forma gratuita y sin ninguna condición.
Nuestra herramienta - Remix IDE
Para el "perfil" del consumo de gas, utilizaremos el entorno en línea para desarrollar los contratos Ethereum del
Remix IDE . Este IDE contiene editor de código de resaltado de sintaxis, visor de artefactos, renderizador de interfaz de contrato, depurador visual de máquina virtual, compiladores JS de todas las versiones posibles y muchas otras herramientas importantes. Recomiendo comenzar el estudio del éter con él. Una ventaja adicional es que no requiere instalación, solo ábralo en un navegador desde el
sitio oficial .
Selección de tipo variable
La especificación del lenguaje Solidity ofrece al desarrollador hasta treinta y dos bits de tipos enteros uint, de 8 a 256 bits. Imagine que está desarrollando un contrato inteligente diseñado para almacenar la edad de una persona en años. ¿Qué profundidad de bit elige?
Sería bastante natural elegir el tipo mínimo suficiente para una tarea específica: uint8 encajaría matemáticamente aquí. Sería lógico suponer que cuanto más pequeño es el objeto que almacenamos en la cadena de bloques y menos memoria gastamos en la ejecución, menos gastos generales tenemos, menos pagamos. Pero en la mayoría de los casos, esta suposición será incorrecta.
Para el experimento, tomamos el contrato más simple de lo
que ofrece la
documentación oficial de
Solidity y lo recopilamos en dos versiones, usando el tipo variable uint256 y el tipo 32 veces más pequeño, 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; } }
Medición de "ahorros"
Por lo tanto, los contratos se crean, cargan en Remix, se implementan y las llamadas a los métodos .set () se ejecutan mediante transacciones. Que vemos
Grabar un tipo largo es más costoso que uno corto : ¡20464 versus 20205 unidades de gas! Como? Por qué ¡Vamos a resolverlo!

Almacenar uint8 vs uint256
Escribir en el almacenamiento persistente es una de las operaciones más costosas en el protocolo por razones obvias: en primer lugar, registrar el estado aumenta el tamaño del espacio en disco requerido por el nodo completo. El tamaño de este almacenamiento aumenta constantemente y cuanto más estados se almacenan en los nodos, cuanto más lenta es la sincronización, mayor es el requisito de infraestructura (tamaño de partición, número de iops). En las horas pico, son las operaciones de E / S de disco lento las que determinan el rendimiento de toda la red.
Sería lógico esperar que el almacenamiento de uint8 debería costar decenas de veces más barato que uint256. Sin embargo, en el depurador puede ver que ambos valores se encuentran exactamente iguales en la ranura de almacenamiento que un valor de 256 bits.

Y en este caso particular, el uso de uint8 no ofrece ninguna ventaja en el costo de escribir en el almacenamiento.
Manejo de uint8 vs uint256
¿Quizás obtendremos beneficios cuando trabajemos con uint8 si no durante el almacenamiento, y al menos cuando manipulemos datos en la memoria? A continuación se comparan las instrucciones para la misma función obtenida para diferentes tipos de variables.

Puede ver que las operaciones con uint8 tienen incluso
más instrucciones que uint256. Esto se debe a que la máquina convierte el valor de 8 bits en una palabra nativa de 256 bits y, como resultado, el código está rodeado de instrucciones adicionales que paga el remitente. No solo escribir, sino también ejecutar código con un tipo uint8 en este caso es más costoso.
¿Dónde se puede justificar el uso de tipos cortos?
Nuestro equipo ha estado involucrado en la auditoría de contratos inteligentes durante mucho tiempo, y hasta ahora no ha habido un solo caso práctico en el que el uso de un tipo pequeño en el código proporcionado para la auditoría conduzca a ahorros. Mientras tanto, en algunos casos muy específicos, los ahorros son teóricamente posibles. Por ejemplo, si su contrato almacena una gran cantidad de pequeñas variables o estructuras de estado, entonces se pueden empaquetar en menos ranuras de almacenamiento.
La diferencia será más evidente en el siguiente ejemplo:
1. contrato con 32 variables 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 con 32 variables 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; } }
La implementación del primer contrato (32 uint256) costará menos, solo 89941 de gas, pero .set () será mucho más costoso ya que Ocupará 256 ranuras de almacenamiento, lo que costará 640,639 de gas por cada llamada. El segundo contrato (32 uint8) será dos veces y media más costoso cuando se implemente (221663 gas), pero cada llamada al método .set () será mucho más barata, porque cambia solo una celda de la etapa (185291 gas).
¿Debería aplicarse tal optimización?
La importancia del efecto de la optimización de tipo es un punto discutible. Como puede ver, incluso para un caso sintético especialmente seleccionado,
no obtuvimos múltiples diferencias. La elección de usar uint8 o uint256 es más bien una ilustración del hecho de que la optimización debe aplicarse de manera significativa (con una comprensión de las herramientas, la creación de perfiles) o no pensar en absoluto. Aquí hay algunas pautas generales:
- si el contrato contiene muchos números pequeños o estructuras compactas en el repositorio, puede pensar en la optimización;
- si usa el tipo "abreviado", recuerde las vulnerabilidades de exceso / disminución de flujo ;
- para variables de memoria y argumentos de función que no están escritos en el repositorio, siempre es mejor usar el tipo nativo uint256 (o su alias uint). Por ejemplo, no tiene sentido establecer el iterador de la lista en uint8, solo perder;
- El orden de las variables en el contrato es de gran importancia para el embalaje correcto en las ranuras de almacenamiento para el compilador.
Referencias
Terminaré con consejos que no tienen contraindicaciones: experimente con herramientas de desarrollo, conozca las especificaciones del lenguaje, la biblioteca y los marcos. Aquí están los enlaces más útiles, en mi opinión, para comenzar a aprender sobre la plataforma Ethereum:
- El entorno de desarrollo de contratos Remix es un IDE muy funcional basado en navegador;
- La especificación del lenguaje Solidity , el enlace irá específicamente a la sección sobre Diseño de variables de estado;
- Un repositorio de contratos muy interesante del famoso equipo OpenZeppelin. Ejemplos de la implementación de tokens, contratos de venta masiva, y lo más importante: la biblioteca SafeMath , la que ayuda a trabajar de forma segura con los tipos;
- Ethereum Yellow Paper , especificación formal de la máquina virtual Ethereum;
- Ethereum White Paper , la especificación de la plataforma Ethereum, un documento más general y de alto nivel con una gran cantidad de enlaces;
- Ethereum en 25 minutos , una breve pero técnica introducción a Ethereum del creador de la plataforma, Vitalik Buterin;
- Etherscan blockchain explorer , una ventana al mundo real de ether, un navegador de bloques, transacciones, tokens, contratos en la red principal. En Etherscan encontrará un explorador para las redes de prueba Rinkeby, Ropsten, Kovan (redes con transmisión gratuita, basadas en diferentes protocolos de consenso).