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

nPythonC (Extensión)Razón
1387.5 ns54.6 ns7
1005.9 μs254 ns23
100072.6 μs2.31 μs31
10000010 ms290 μs34
1000000132 ms3.91 ms34

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

nPythonCPyO3
1387.5 ns54.6 ns91.2 ns
1005.9 µs254 ns287 ns
100072.6 µs2.31 µs2.12 µs
10000010 ms290 µs264 µs
1000000132 ms3.91 ms3.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