micropython-optimizacion-native-viper

Optimización de MicroPython con @native y @viper

  • 5 min

Los decoradores @micropython.native y @micropython.viper son herramientas para compilar funciones críticas a código más rápido.

Una de las críticas que siempre escuchamos sobre MicroPython es: “Es lento porque es interpretado”. Y, en parte, tienen razón.

En el funcionamiento normal, MicroPython compila tu código .py a Bytecode. Luego, una Máquina Virtual (VM) lee ese bytecode y le dice al procesador qué hacer. Este intermediario aporta flexibilidad, pero roba ciclos de CPU.

Pero MicroPython guarda un as bajo la manga. Podemos saltarnos la Máquina Virtual y pedirle al compilador que genere Código Máquina Real (instrucciones binarias que el procesador entiende directamente).

Para ello, usamos dos decoradores especiales: @micropython.native y @micropython.viper.

@micropython.native: El acelerador fácil

El emisor nativo es la forma más sencilla de ganar velocidad. Simplemente colocamos el decorador encima de nuestra función.

Lo que hace es tomar el cuerpo de la función y compilarlo a código ensamblador de la arquitectura que estemos usando (ARM Thumb para RP2040/STM32, o Xtensa para ESP32).

Características

  • Velocidad: Aproximadamente 2x más rápido que el código normal.
  • Compatibilidad: Soporta casi todas las características de Python (bucles, with, manejo de excepciones, etc.).
  • Coste: El código compilado ocupa más memoria RAM que el bytecode (aprox. el doble de tamaño).

Ejemplo de uso

Imaginemos una función costosa, como calcular la sucesión de Fibonacci de forma recursiva (ineficiente a propósito para notar la carga).

import time

# Versión estándar (Bytecode)
def fib_normal(n):
    if n < 2: return n
    return fib_normal(n-1) + fib_normal(n-2)

# Versión Nativa
@micropython.native
def fib_native(n):
    if n < 2: return n
    return fib_native(n-1) + fib_native(n-2)

# --- Benchmark ---
start = time.ticks_us()
fib_normal(20)
print("Normal:", time.ticks_diff(time.ticks_us(), start), "us")

start = time.ticks_us()
fib_native(20)
print("Nativa:", time.ticks_diff(time.ticks_us(), start), "us")
Copied!

Si ejecutas esto, verás que la versión nativa tarda aproximadamente la mitad. ¡Solo por añadir una línea!

@micropython.viper: Velocidad absurda

Si native es un coche deportivo, viper es un Fórmula 1 sin frenos ni cinturón de seguridad.

Viper (Variable Integers PointERs) optimiza el código asumiendo tipos de datos. Mientras que en Python todo es un objeto dinámico, Viper trata de usar tipos enteros puros y punteros de memoria, eliminando casi toda la sobrecarga de Python.

Características

  • Velocidad: Puede ser entre 5x y 50x más rápido. Se acerca a la velocidad de C.
  • Tipado: Necesita que le digas qué tipos de datos entran y salen (Type Hints).
  • Limitaciones:
  • Trabaja con enteros de 31 bits (el bit 32 se usa para marcar que es un entero y no un objeto).
  • No soporta todas las funciones de Python dentro.
  • Permite manejo directo de Punteros (ptr).

Sintaxis de Viper

En Viper, debemos definir los tipos de los argumentos. Si no lo hacemos, asume que son objetos genéricos y perdemos velocidad.

@micropython.viper
def suma_rapida(a: int, b: int) -> int:
    return a + b
Copied!

Acceso mediante punteros (ptr)

La verdadera bestia de Viper aparece cuando queremos manipular GPIOs o buffers de memoria directamente, saltándonos las capas de abstracción de machine.Pin.

Imaginemos que queremos cambiar el estado de un pin (bit-banging) lo más rápido posible.

import machine

# Ejemplo conceptual (Las direcciones dependen del chip)
# GPIO_OUT_REG es la dirección de memoria donde se escriben los pines
GPIO_OUT_REG = 0x3FF44004 # Ejemplo para ESP32 (¡Verificar datasheet!)

@micropython.viper
def parpadeo_extremo():
    # Creamos un puntero a un entero de 32 bits (ptr32)
    # apuntando a la dirección del registro
    p = ptr32(GPIO_OUT_REG)
    
    # Bucle frenético
    for i in range(1000000):
        p[0] = 1 # Escribe en la memoria directamente (High)
        p[0] = 0 # Escribe en la memoria directamente (Low)
Copied!

¡Cuidado con Viper! Viper desactiva muchas protecciones de seguridad. Si accedes a una dirección de memoria incorrecta usando punteros, o te sales del índice de un array, puedes provocar un Segmentation Fault y reiniciar el microcontrolador al instante. Es programación “a pelo”.

Comparativa de rendimiento

Vamos a hacer una prueba sencilla: sumar números en un bucle un millón de veces.

import time

MAX = 1000000

def loop_normal():
    x = 0
    for i in range(MAX):
        x += 1
    return x

@micropython.native
def loop_native():
    x = 0
    for i in range(MAX):
        x += 1
    return x

@micropython.viper
def loop_viper():
    x = 0
    # En viper el rango debe ser manejado con cuidado o usar while
    # para máxima velocidad pura con enteros
    i = 0
    while i < MAX:
        x += 1
        i += 1
    return x

# Resultados típicos en un ESP32 a 240MHz:
# Normal:  ~4500 ms
# Native:  ~2600 ms (x1.7 más rápido)
# Viper:   ~150  ms (x30 más rápido)
Copied!

La diferencia es abismal. Viper convierte el bucle en unas pocas instrucciones de ensamblador.

¿Cuándo usar cada uno?

  1. Código Normal: El 95% de tu programa. Configuración, WiFi, lógica de alto nivel, interfaces.
  2. @micropython.native: Cuando tienes una función que hace muchos cálculos matemáticos y notas que va un poco lenta. Es el primer paso de optimización porque es seguro y fácil.
  3. @micropython.viper:
  • Drivers de pantallas LCD/TFT (mover píxeles rápido).
  • Generación de señales por software (bit-banging).
  • Lectura de sensores a altísima velocidad.
  • Cálculos matemáticos pesados (FFT, filtros digitales).

Desventajas a considerar

No todo es gratis. Usar estos emisores tiene un coste:

  1. Uso de RAM: El código máquina ocupa más espacio que el bytecode. Si estás muy justo de RAM en un ESP8266, quizás no puedas usar native.
  2. Portabilidad: El código Viper con punteros directos a registros no es portable. Si escribes en el registro 0x3FF... del ESP32, ese código no funcionará en una Raspberry Pi Pico.
  3. Tiempo de compilación: Al importar el módulo, tarda un poco más en arrancar porque tiene que compilar en ese momento.

MicroPython es flexible, pero cuando necesitas potencia bruta, native y viper te permiten quitarle las “ruedines” a la bicicleta y acelerar las partes críticas de tu código.