Deep EVM #2: Modelo de Memoria — Stack, Memory, Storage y Calldata
Engineering Team
Las cuatro ubicaciones de datos de la EVM
La EVM tiene cuatro ubicaciones distintas donde se pueden almacenar datos, cada una con características de coste y persistencia muy diferentes. Elegir la ubicación incorrecta puede convertir una operación de $0.50 en una de $50.
Stack
El stack es la ubicación más barata y más restringida. Tiene una profundidad máxima de 1024 elementos, y cada elemento es una palabra de 256 bits. Los opcodes PUSH, DUP y SWAP manipulan el stack directamente. El coste es de solo 3 gas por operación.
El stack es perfecto para valores temporales durante cálculos, pero no puedes acceder a elementos en posiciones arbitrarias — solo los 16 superiores son accesibles vía DUP y SWAP.
Memory (Memoria volátil)
La memoria es un arreglo de bytes expandible que existe solo durante la ejecución del call context actual. Se accede en fragmentos de 32 bytes usando MLOAD y MSTORE, o byte a byte con MSTORE8.
El coste de la memoria crece cuadráticamente: las primeras palabras son baratas, pero expandir la memoria a tamaños grandes se vuelve extremadamente costoso. La fórmula es:
coste_memoria = (palabras_memoria² / 512) + (3 * palabras_memoria)
Por ejemplo, los primeros 724 bytes cuestan solo gas lineal. Pero expandir a 10KB cuesta significativamente más debido al término cuadrático. Esta es la razón por la que nunca debes asignar grandes buffers de memoria en contratos.
Storage (Almacenamiento persistente)
El storage es la ubicación más cara — es donde los datos persisten entre transacciones. Cada contrato tiene su propio espacio de almacenamiento, un mapa clave-valor donde tanto claves como valores son de 256 bits.
// Costes de storage (post EIP-2929 + EIP-3529):
// SLOAD frío: 2100 gas
// SLOAD caliente: 100 gas
// SSTORE (0 → no-cero, frío): 22100 gas
// SSTORE (no-cero → no-cero, caliente): 100 gas
// Reembolso por limpiar slot: 4800 gas
El layout de storage en Solidity es determinista: las variables de estado se asignan secuencialmente a slots comenzando desde el slot 0. Variables más pequeñas que 256 bits se empaquetan en el mismo slot cuando es posible.
Calldata
Calldata son los datos de entrada enviados con una transacción o llamada. Es de solo lectura e inmutable. Cuesta 16 gas por byte no cero y 4 gas por byte cero — significativamente más barato que la memoria para operaciones de lectura.
En Solidity, los parámetros marcados como calldata en funciones externas evitan copiar datos a memoria, ahorrando gas.
Empaquetamiento de variables de almacenamiento
Una de las optimizaciones más importantes es el empaquetamiento de variables de storage. La EVM opera en palabras de 32 bytes, así que variables más pequeñas pueden compartir un slot:
// Malo: 3 slots (3 * SSTORE = caro)
contract Ineficiente {
uint256 a; // slot 0 (32 bytes)
uint8 b; // slot 1 (1 byte, pero ocupa slot completo)
uint256 c; // slot 2 (32 bytes)
}
// Bueno: 2 slots
contract Eficiente {
uint256 a; // slot 0
uint256 c; // slot 1
uint8 b; // slot 1 (empaquetado con c? No - slot 2)
}
// Mejor: empaquetamiento explícito
contract MejorAun {
uint128 a; // slot 0, primera mitad
uint128 b; // slot 0, segunda mitad
uint256 c; // slot 1
}
El puntero de memoria libre
Solidity mantiene un “puntero de memoria libre” en la posición 0x40. Este puntero rastrea dónde comienza la memoria no utilizada. Cada vez que Solidity necesita asignar memoria, lee el puntero, escribe los datos ahí, y avanza el puntero.
// Leer el puntero de memoria libre en Yul:
assembly {
let ptr := mload(0x40)
// ptr ahora apunta al inicio de memoria libre
mstore(ptr, 42) // Escribir el valor 42
mstore(0x40, add(ptr, 32)) // Avanzar el puntero
}
Entender este mecanismo es crucial para escribir Yul optimizado, donde gestionas la memoria manualmente.
Returndata y code
Además de las cuatro ubicaciones principales, hay dos más:
- Returndata — Los datos devueltos por la última llamada externa. Accesible vía RETURNDATASIZE y RETURNDATACOPY. Es temporal y se sobrescribe con cada llamada externa.
- Code — El bytecode del contrato en ejecución. Accesible vía CODESIZE y CODECOPY. Es inmutable y se puede usar para almacenar datos constantes directamente en el bytecode.
Implicaciones prácticas
La elección de ubicación de datos tiene consecuencias masivas en coste:
- Usa calldata para parámetros de funciones externas — Es más barato que copiar a memory
- Cachea lecturas de storage en variables locales — Una lectura de storage fría cuesta 2100 gas; una lectura de stack cuesta 3 gas
- Empaqueta variables de storage — Reduce el número de slots y por tanto de operaciones SSTORE
- Evita asignar memoria grande — El coste cuadrático te penalizará
- Usa eventos para datos que solo necesitan ser leídos off-chain — Los logs son mucho más baratos que el storage
Conclusión
Las cuatro ubicaciones de datos de la EVM — stack, memory, storage y calldata — cada una existe para un propósito específico con un perfil de coste diferente. Entender cuándo usar cada una es la diferencia entre un contrato eficiente y uno que desperdicia el dinero de los usuarios en gas innecesario.