Ir al contenido principal
BlockchainMar 28, 2026

Deep EVM #7: Bucles y Condicionales Eficientes en Gas en Yul

OS
Open Soft Team

Engineering Team

Flujo de control en Yul

Yul proporciona tres construcciones de flujo de control: if, switch, y for. Cada una compila a diferentes patrones de opcodes JUMP y JUMPI, con implicaciones distintas en el coste de gas.

El condicional if

El if en Yul no tiene else. Para bifurcaciones completas, usa switch:

assembly {
    let x := calldataload(0)
    
    // If simple — sin else
    if gt(x, 100) {
        x := 100 // Cap en 100
    }
    
    // Para if/else, usa switch
    switch gt(x, 50)
    case 1 {
        // x > 50
        mstore(0x00, 1)
    }
    default {
        // x <= 50
        mstore(0x00, 0)
    }
}

Switch/case para despacho de funciones

El switch es especialmente útil para despacho de funciones, que es el patrón más común en contratos Yul standalone:

switch shr(224, calldataload(0))
case 0x70a08231 /* balanceOf */ {
    returnUint(sload(mappingSlot(calldataload(4), 0)))
}
case 0xa9059cbb /* transfer */ {
    ejecutarTransfer(calldataload(4), calldataload(36))
}
case 0x18160ddd /* totalSupply */ {
    returnUint(sload(0))
}
default {
    revert(0, 0)
}

Cada case compila a un PUSH4 + EQ + PUSH2 + JUMPI. Con N funciones, el peor caso es N comparaciones. Para contratos con muchas funciones, esto es O(N) — las jump tables de Huff son más eficientes.

Bucles for

El bucle for en Yul tiene la misma estructura que en C:

assembly {
    // for (init; cond; post) { body }
    for { let i := 0 } lt(i, 10) { i := add(i, 1) } {
        // cuerpo del bucle
        mstore(add(0x80, mul(i, 32)), i)
    }
}

Optimización: incremento pre vs post

En Solidity, ++i es más barato que i++ porque evita una copia temporal. En Yul, esto no importa — add(i, 1) es todo lo que hay.

Optimización: cachear la longitud

// Malo: lee array.length de storage en cada iteración
for { let i := 0 } lt(i, sload(lengthSlot)) { i := add(i, 1) } {
    // sload cada iteración = 100 gas (caliente) * N
}

// Bueno: cachear en variable local
let len := sload(lengthSlot) // 2100 gas una vez (frío)
for { let i := 0 } lt(i, len) { i := add(i, 1) } {
    // lt usa variable de stack = 3 gas * N
}

Iteración sobre arrays de storage

Los arrays dinámicos en Solidity almacenan la longitud en el slot base y los elementos a partir de keccak256(slot):

assembly {
    // Array dinámico en slot 2
    let slotBase := 2
    let length := sload(slotBase)
    
    // Los elementos comienzan en keccak256(slotBase)
    mstore(0x00, slotBase)
    let dataSlot := keccak256(0x00, 0x20)
    
    let suma := 0
    for { let i := 0 } lt(i, length) { i := add(i, 1) } {
        suma := add(suma, sload(add(dataSlot, i)))
    }
    
    mstore(0x00, suma)
    return(0x00, 0x20)
}

Iteración sobre calldata

Calldata es la fuente de datos más barata para iterar:

assembly {
    // Función: sumarArray(uint256[] calldata nums)
    // Calldata layout: [selector(4)] [offset(32)] [length(32)] [elem0(32)] [elem1(32)] ...
    let offset := add(calldataload(4), 4) // Offset del array
    let length := calldataload(offset)
    let dataStart := add(offset, 32)
    
    let suma := 0
    for { let i := 0 } lt(i, length) { i := add(i, 1) } {
        suma := add(suma, calldataload(add(dataStart, mul(i, 32))))
    }
    
    mstore(0x00, suma)
    return(0x00, 0x20)
}

Cada calldataload cuesta solo 3 gas, comparado con 100+ gas para sload caliente.

Desenrollado de bucles

Para bucles con conteo conocido y pequeño, el desenrollado manual puede ahorrar el overhead del condicional y el incremento:

// Bucle normal: 5 iteraciones con overhead por iteración
for { let i := 0 } lt(i, 5) { i := add(i, 1) } {
    // lt(3) + add(3) + jumpi(10) = 16 gas overhead por iteración
    proceso(sload(add(base, i)))
}

// Desenrollado: sin overhead de bucle
proceso(sload(base))
proceso(sload(add(base, 1)))
proceso(sload(add(base, 2)))
proceso(sload(add(base, 3)))
proceso(sload(add(base, 4)))

El desenrollado ahorra ~16 gas por iteración eliminada, pero aumenta el tamaño del bytecode. Úsalo solo cuando el conteo de iteraciones es fijo y pequeño (< 8).

Patrones avanzados

Búsqueda binaria en array ordenado

function busquedaBinaria(base, len, objetivo) -> encontrado, indice {
    let bajo := 0
    let alto := len
    
    for {} lt(bajo, alto) {} {
        let medio := shr(1, add(bajo, alto))
        let valor := sload(add(base, medio))
        
        switch lt(valor, objetivo)
        case 1 { bajo := add(medio, 1) }
        default {
            switch gt(valor, objetivo)
            case 1 { alto := medio }
            default {
                encontrado := 1
                indice := medio
                bajo := alto // Salir del bucle
            }
        }
    }
}

Conclusión

El flujo de control eficiente en Yul se reduce a minimizar opcodes JUMP (8 gas) y JUMPI (10 gas), cachear valores del storage, y elegir la fuente de datos correcta (calldata > memory > storage). Dominar estos patrones es fundamental para escribir contratos Yul de alto rendimiento.