Este es probablemente el ejemplo que mejor resume por qué existe Rust. Para repartir un bucle entre todos los núcleos del procesador no hay que crear hilos, ni repartir el trabajo, ni sincronizar nada: se cambia iter() por par_iter(). Y si el cambio introdujera una carrera de datos, el programa no compilaría.
También te puede interesar
La dependencia
cargo add rayon
[dependencies] rayon = "1.12.0"
El mismo cálculo, en serie y en paralelo
use rayon::prelude::*;
use std::time::Instant;
fn es_primo(n: u64) -> bool {
if n < 2 { return false; }
let mut d = 2;
while d * d <= n {
if n % d == 0 { return false; }
d += 1;
}
true
}
fn main() {
let numeros: Vec<u64> = (2..600_000).collect();
let t = Instant::now();
let en_serie = numeros.iter().filter(|&&n| es_primo(n)).count();
let serie = t.elapsed();
let t = Instant::now();
let en_paralelo = numeros.par_iter().filter(|&&n| es_primo(n)).count();
let paralelo = t.elapsed();
println!("primos encontrados: {en_serie} (en serie) y {en_paralelo} (en paralelo)");
println!("en serie : {:.2?}", serie);
println!("en paralelo: {:.2?}", paralelo);
println!("{:.1} veces más rápido en {} núcleos",
serie.as_secs_f64() / paralelo.as_secs_f64(),
rayon::current_num_threads());
}
cargo run --releaseprimos encontrados: 49098 (en serie) y 49098 (en paralelo) en serie : 46.00ms en paralelo: 6.82ms 6.7 veces más rápido en 16 núcleos
La única diferencia entre las dos versiones es iter() frente a par_iter(). Todo lo demás —el filter, el count— es idéntico. rayon reparte el vector entre los núcleos disponibles, va robando trabajo de los hilos que terminan antes, y junta los resultados.
Los tiempos dependen de la máquina: esta tiene 16 núcleos y salen 6,7 veces más rápido, no 16, porque repartir y juntar también cuesta. En una máquina de cuatro núcleos la mejora será menor, y el programa seguirá funcionando igual sin tocar una línea.
--release. En modo de depuración las comprobaciones adicionales distorsionan la comparación y los dos tiempos se parecen más de lo que deberían. Para cualquier medición de rendimiento, cargo run --release; hay más sobre esto en el ejemplo de medir el rendimiento.Por qué esto es seguro
En otros lenguajes, paralelizar un bucle obliga a revisar a mano si el cuerpo toca algo compartido. Aquí no hace falta revisarlo porque el compilador no te deja equivocarte: para que par_iter() acepte tu cierre, lo que haya dentro tiene que poder cruzar hilos, y eso se comprueba con los traits Send y Sync de la lección de concurrencia.
Si el cuerpo del bucle modificara una variable compartida sin protección, el programa no compilaría. Esa es la diferencia real: no es que rayon sea cómodo, es que el cambio de una línea no puede introducir un error silencioso.
Lo que se usa a diario
| En serie | En paralelo |
|---|---|
v.iter() |
v.par_iter() |
v.iter_mut() |
v.par_iter_mut() |
v.into_iter() |
v.into_par_iter() |
for x in v |
v.par_iter().for_each(|x| ...) |
v.sort() |
v.par_sort() |
| dos tareas seguidas | rayon::join(a, b) |
El código de esta página se compiló y ejecutó con rustc 1.96.1 y rayon 1.12.0 antes de publicarla, en modo release sobre una máquina de 16 núcleos; los tiempos están copiados de esa ejecución y variarán en otra máquina. Documentación oficial: rayon en docs.rs.