Extiende Python fácilmente usando Rust

adriandelgado

Diapositivas

adrianjdelgado.com/2023/flisol-rust-py
█▀▀▀▀▀█ ▄ █ ▀█▄▄██▀▀▄ █▀▀▀▀▀█
█ ███ █ ▄▄▄ █▄█▄▄▄▀█▀ █ ███ █
█ ▀▀▀ █ ▄█ ▄▄▀▄▄█▄█▀▀ █ ▀▀▀ █
▀▀▀▀▀▀▀ ▀▄█▄▀ ▀▄▀ ▀ █ ▀▀▀▀▀▀▀
██▀██ ▀▀▀▀▄▀▄█  ▄█▄▄▄█▄█▄▀ ▀▄
 ▄████▀   ▀ ▄ ██▄▀  ▀▀ ▀▀▄ ▄
▀  ▀▀▄▀ ▄▀▀ ▀▀█ ▄█▄▄▄▄▄▄▄▀▀ ▄
 ▀█▄██▀▄ ▀ █▀▀ ▀▄  ▄█▄█ ▄▀▀▄
▄██  ▄▀██▄ ▀█▀█▀▄██▄▄▄▄█▄▀█ ▄
█ ▀ ██▀▀ █▄ ▄ █▀█▀  ▄██▄█ ▀▄
▀  ▀▀ ▀▀██▄█ ▀█  ▀▀▀█▀▀▀█▄███
█▀▀▀▀▀█ ▀▀▄ █▀▄▀▀▀███ ▀ █▀▀▄▄
█ ███ █ █▄▀▄ ▀█▀▀▀▀▄▀▀▀▀▀▄█▄█
█ ▀▀▀ █ █ ▄   ▀▀  █▀▄▀▀███▀█
▀▀▀▀▀▀▀ ▀▀ ▀▀▀ ▀▀▀▀▀ ▀  ▀ ▀

Frontend de lenguajes de sistemas

  • C (Numpy, Librería estandar)
  • Fortran (Scipy)
  • C++ (PyTorch, OpenCV)

Criba de Eratóstenes: Python

 
import math


def sieve(n):
    numbers = list(range(2, n + 1))

    for i in range(2, int(math.sqrt(n))):
        if numbers[i - 2] != 0:
            for j in range(i * i, n + 1, i):
                numbers[j - 2] = 0

    return [x for x in numbers if x != 0]
         

Output:

 
>>> sieve(5)
[2, 3, 5]

>>> sieve(20)
[2, 3, 5, 7, 11, 13, 17, 19]
         

C API



static PyObject *sieve_impl(PyObject *self, PyObject *max_n) {
    size_t n;
    if ((n = PyLong_AsSize_t(max_n)) == (size_t)-1 && PyErr_Occurred()) { return NULL; }

    // Populate the C array
    int *sieve = malloc((n - 1) * sizeof(int));
    if (sieve == NULL) {
        PyErr_NoMemory(); // raise MemoryError()
        return NULL;
    }

    for (size_t i = 2; i < n + 1; ++i) { sieve[i - 2] = (int)i; }

    // Sieve out composite numbers
    size_t lim = (size_t)sqrt((double)n);
    for (size_t i = 2; i < lim; ++i) {
        if (sieve[i - 2] != 0) {
            for (size_t j = (i * i); j < (n + 1); j += i) {
                sieve[j - 2] = 0;
            }
        }
    }

    // Convert to Python list
    size_t num_primes = 0; // Calculate total size of list
    for (size_t i = 0; i < n - 1; ++i) { if (sieve[i]) { num_primes++; } }

    PyObject *rv = PyList_New(num_primes);
    if (rv == NULL) { goto cleanup; }
    PyObject *obj = NULL;
    size_t j = 0;
    for (size_t i = 0; i < n - 1; ++i) {
        if (!sieve[i]) { continue; }
        if ((obj = PyLong_FromLong(sieve[i])) == NULL || // int -> Py int
                   PyList_SetItem(rv, j++, obj)) {       // rv[i] = obj
            Py_DECREF(rv); rv = NULL;                    // On error, remove list
            goto cleanup;
        }
    }
cleanup:
    free(sieve);
    return rv;
} 

Desventajas de la C API

  • Manejo de memoria manual
  • Conteo de referencias manual (Py_INCREF, Py_DECREF)
  • Incómodo manejo de errores/excepciones (PyErr_*)

Ventaja de la C API

n Python C (Extensión) Razón
1 387.5 ns 54.6 ns 7
100 5.9 μs 254 ns 23
1000 72.6 μs 2.31 μs 31
100000 10 ms 290 μs 34
1000000 132 ms 3.91 ms 34

Rust LogoRust

  • Stack Overflow "most loved language" 2016 - 2022 (7 años consecutivos)
  • Alto Rendimiento
  • Fiable
  • Seguridad de hilos
  • Productivo

¿Quiénes están usando Rust?

Amazon (AWS)

  • Firecracker (Lambda)
  • Tokio (async runtime)
  • Internamente (S3, EC2, Cloudfront, Route 53)

Google

  • Android
  • Fuchsia
  • Chromium

Rust for Linux

… on the whole I don't hate it.
—Linus Torvalds

¡Muchos más!

  • Mozilla (Firefox)
  • Cloudflare (Quiche, BoringTun, Pingora*, Oxy*)
  • Tor Project (Arti)
  • Herramientas CLI:
    • Ruff (Python Linter 10-100x más rápido que alt.)
    • Ripgrep (3-10x más rápido que grep)
    • Meilisearch (Alternativa a ElasticSearch)
  • Vercel, Meta, Dropbox, Microsoft, Samsung, Figma, 1Password, GitHub, npm, Canonical …

PyO3

Python 🤝 Rust

Rust: PyO3

 
/// Usa la criba de Eratóstenes para calcular los números primos
/// menores o iguales a `n`
#[pyfunction]
fn sieve(n: usize) -> Vec<u32> {
    let mut sieve: Vec<u32> = (2..=(n as u32)).collect();
    let lim: usize = (n as f64).sqrt() as usize;

    for i in 2..=lim {
        if sieve[i - 2] != 0 {
            let mut j = i * i;
            while j < n + 1 {
                sieve[j - 2] = 0;
                j += i;
            }
        }
    }

    sieve.retain(|&x| x != 0);

    sieve
}

/// Esta es la documentación del módulo
#[pymodule]
fn charla_flisol_rust(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(sieve, m)?)?;
    Ok(())
}   

Con Rust

  • Fácil manejo de memoria
  • Conteo de referencias automático
  • Cómodo manejo de errores/excepciones
  • Multiples hilos (de verdad)

Gotta go fast

n Python C PyO3
1 387.5 ns 54.6 ns 91.2 ns
100 5.9 µs 254 ns 287 ns
1000 72.6 µs 2.31 µs 2.12 µs
100000 10 ms 290 µs 264 µs
1000000 132 ms 3.91 ms 3.69 ms

¿Quiénes usan PyO3?

Demo

Futuro de Rust

Crecimiento Exponencial

Languish Rust Stats
Fuente: tjpalmer.github.io/languish

Alcanzando a los establecidos

Languish Rust vs Go vs C Stats
Fuente: tjpalmer.github.io/languish

Links relevantes