wandres.dev
RENDIMIENTO · zero-cost, SIMD

Los mitos del rendimiento: por qué el idiomático es rápido y la regla del 3 %

Los mitos que sabotean al programador de Rust: que el idiomático es lento, que toda abstracción cuesta, que unsafe acelera. La verdad: los iteradores igualan o baten al índice manual, la mayoría de las abstracciones son de coste cero y las pocas que cuestan se nombran una a una, y la regla de oro de Knuth manda perfilar el 3 % crítico y dejar en paz el 97 % restante.

⏱ 17 min

El nivel se cierra desmontando los mitos que más rendimiento le cuestan al programador de Rust, y son casi todos culturales, no técnicos. El mito de que el código idiomático —iteradores, Option, genéricos— es un lujo que se paga en velocidad. El mito de que toda abstracción cuesta, que empuja a evitar las que en realidad son gratis. El mito de que unsafe es más rápido, que cambia riesgo real por ganancia imaginaria. Frente a ellos, una síntesis y una regla. La síntesis: el idiomático suele ser óptimo porque las abstracciones de Rust son de coste cero (lección 1), y las pocas que sí cuestan se pueden nombrar una por una. La regla, la más antigua y la más ignorada, es la de Knuth: optimiza el 3 % que importa y deja tranquilo el 97 % restante —pero solo el perfilador sabe cuál es cuál—.

🎯 Al terminar esta lección sabrás
  • Desmontar el mito del idiomático lento con el ejemplo del iterador frente al índice.
  • Separar las abstracciones de coste cero de las que tienen un coste real y nombrarlo.
  • Enunciar la regla de Knuth y ligarla a la ley de Amdahl y al flujo de trabajo.
  • Articular el orden correcto: claridad primero, medición después, optimización solo donde el perfil manda.

El mito del idiomático lento

El prejuicio más caro es creer que para ir rápido hay que renunciar a los iteradores y volver a los bucles con índice, como si la expresividad fuera un impuesto. Es falso, y lo viste en la lección 1: gracias a la monomorfización y al inlining, una cadena de iteradores compila al mismo bucle ceñido que el índice manual. Pero el mito no solo es falso: a menudo está invertido. El iterador es con frecuencia más rápido que el índice, porque garantiza por construcción que cada acceso está dentro de rango y le ahorra al programa las comprobaciones de límites que v[i] obliga a insertar cuando el optimizador no puede probar la cota.

// El indice fuerza a comprobar la cota si el optimizador no la demuestra.
fn maximo_indexado(v: &[i32]) -> i32 {
    let mut m = i32::MIN;
    for i in 0..v.len() { if v[i] > m { m = v[i]; } }
    m
}

// El iterador prueba la cota por construccion: sin comprobaciones, vectorizable.
fn maximo_iter(v: &[i32]) -> i32 {
    v.iter().copied().max().unwrap_or(i32::MIN)
}

Reescribir un for i in 0..v.len() como un for x in &v no es azúcar estético: le entrega al compilador una prueba que el índice le ocultaba, y esa prueba se traduce en comprobaciones eliminadas y bucles que el vectorizador se atreve a tocar. En Rust, escribir idiomático es, la mayoría de las veces, escribir rápido.

El coste real de las abstracciones

El mito complementario —que toda abstracción cuesta— es igual de dañino, porque lleva a evitar herramientas gratuitas por un miedo mal dirigido. La verdad tiene dos columnas, y el ingeniero maduro las distingue de memoria. La mayoría de las abstracciones de Rust son de coste cero: genéricos, iteradores, Option y Result, closures, newtypes, Drop. No indireccionan, no asignan, no dejan rastro en ejecución. Frente a ellas, un puñado tiene un coste real y conocido —y elegirlas es legítimo, siempre que sepas lo que pagas—.

Coste cero · úsalas sin miedo

Genéricos e iteradores (monomorfizados), Option/Result (optimización de nicho), closures, newtypes repr(transparent), Drop. Compilan hasta desaparecer.

💸

Coste real · elígelas sabiendo

dyn Trait (indirección por vtable, sin inline), Arc (contador atómico), RefCell (comprobación de préstamo en ejecución), Box (indirección al heap), Mutex (sincronización).

Fíjate en que ninguno de los costes de la derecha es escandaloso: una indirección de puntero, un incremento atómico, una comprobación booleana. Son baratos en absoluto, y en la inmensa mayoría del código su precio es invisible. El error no es usarlos, es usarlos en una ruta caliente sin saber que cuestan —un Arc::clone en un bucle cerrado, un dyn en el centro de un cálculo numérico— y luego culpar al lenguaje de una lentitud que tú elegiste. Y el tercer mito, el de que unsafe acelera, se cae solo con lo anterior: el optimizador ya elimina las comprobaciones de límites del código idiomático, así que bajar a punteros crudos rara vez compra velocidad y siempre compra riesgo de comportamiento indefinido. Se mide primero; casi nunca compensa.

⚠️
Los costes que la promesa de coste cero no cubre

“Coste cero” habla del tiempo de ejecución, y ser honesto exige nombrar lo que sí cuesta en otras monedas. La monomorfización (nivel 14) infla el tiempo de compilación y el tamaño del binario —cada instanciación es una copia—. Algunas abstracciones, async la primera, imponen un coste cognitivo real aunque su ejecución sea eficiente. Y el reordenamiento de campos, la niche optimization y demás son ejecución barata pero pueden complicar la interoperabilidad con C (de ahí repr(C)). Coste cero no es “gratis en todos los ejes”; es “gratis en ejecución”, que es el eje que tus usuarios pagan.

La regla de oro: perfila el 3 % que importa

Todo el nivel converge en una sentencia que Donald Knuth escribió en 1974 y que el tiempo no ha desgastado: «deberíamos olvidarnos de las pequeñas eficiencias, digamos el 97 % del tiempo: la optimización prematura es la raíz de todo mal. Y sin embargo no deberíamos desaprovechar nuestras oportunidades en ese 3 % crítico». La frase suele citarse a medias —solo la parte de la raíz del mal— y por eso se malentiende: Knuth no predicaba ignorar el rendimiento, sino concentrarlo donde rinde. El corolario es la ley de Amdahl: si una función ocupa el 5 % del tiempo, hacerla infinitamente rápida mejora el programa un 5 % como mucho. Optimizar fuera del punto caliente no es virtud, es desperdicio.

flowchart TD
A[Escribe codigo idiomatico y claro primero] --> B[Perfila para hallar el 3 por ciento caliente]
B --> C{Existe un punto caliente real}
C -->|no| D[Ya terminaste el idiomatico bastaba]
C -->|si| E[Optimiza solo ese punto medido]
E --> F[Verifica con criterion que de verdad mejoro]
F --> B
style A fill:#cba6f7,color:#11111b
style D fill:#a6e3a1,color:#11111b
style E fill:#fab387,color:#11111b

El flujo que ordena el nivel entero es este. Escribe claro e idiomático primero, confiando en que las abstracciones de coste cero te dan velocidad casi óptima sin esfuerzo. Perfila una carga real (lección 2) para encontrar el 3 % donde el tiempo se concentra —que casi nunca es donde tu intuición apuntaba—. Optimiza solo ese punto, con las técnicas del nivel: reducir asignaciones (lección 4), mejorar el layout (nivel 19), y solo si el perfil lo exige, SIMD (lección 3). Verifica con criterion que el cambio fue mejora y no ruido. Y vuelve a perfilar. La claridad no es enemiga de la velocidad: es su punto de partida, porque el código legible es el que puedes perfilar, entender y optimizar con cirugía en vez de con hacha.

La madurez en rendimiento es saber qué NO optimizar

El arco entero de este nivel dibuja una progresión que va del entusiasmo del principiante a la serenidad del ingeniero maduro, y su lección final es contraintuitiva: dominar el rendimiento consiste, sobre todo, en saber qué dejar en paz. El principiante que descubre SIMD, los intrinsics, unsafe y los trucos de bits quiere aplicarlos en todas partes, y produce código más rápido en microbenchmarks que nadie ejecuta y más lento de entender, mantener y —paradoja cruel— de volver a optimizar cuando el cuello de botella real por fin aparece en otra parte. Knuth vio esto hace medio siglo con una claridad que la industria sigue sin asimilar: el rendimiento de un programa no está repartido, está brutalmente concentrado, y la inmensa mayoría del código —ese 97 %— es irrelevante para la velocidad y solo debería optimizarse para la claridad. Gastar ingenio en acelerar código frío no es diligencia, es una forma sofisticada de procrastinación que además degrada lo que toca. La disciplina madura invierte el instinto: primero escribes lo más claro que puedas, precisamente porque no sabes aún dónde estará el problema, y la claridad es lo que te permitirá encontrarlo y arreglarlo; luego mides, con la humildad de aceptar que tu intuición sobre el punto caliente es probablemente errónea; y solo entonces, sobre el 3 % que el perfilador señaló con el dedo, despliegas la artillería —y descubres, casi siempre, que las optimizaciones idiomáticas de coste cero ya te habían dado el noventa por ciento del rendimiento gratis, y que la artillería pesada sobra—. Aquí es donde Rust regala su ventaja definitiva: en un lenguaje cuyas abstracciones son de coste cero, “escribir claro” y “escribir rápido” dejan de ser objetivos en tensión y se vuelven, la mayor parte del tiempo, el mismo acto. El programador que ha interiorizado el nivel no pregunta “¿cómo hago esto más rápido?” ante cada línea; escribe idiomático, perfila cuando algo va lento de verdad, y reserva su ingenio para el puñado de sitios donde de verdad mueve la aguja. Saber optimizar es valioso; saber cuándo no hacerlo es sabiduría.

📝
Lo esencial de los mitos y la regla de oro

Tres mitos, tres verdades. El idiomático no es lento: los iteradores igualan o baten al índice manual eliminando comprobaciones de límites. No toda abstracción cuesta: la mayoría (genéricos, iteradores, Option, closures) son de coste cero, y las que cuestan —dyn, Arc, RefCell, Box, Mutex— tienen un precio pequeño y conocido que solo importa en ruta caliente. unsafe rara vez acelera, porque el optimizador ya elide las comprobaciones del código seguro. Sobre todo manda la regla de Knuth: optimización prematura es la raíz del mal, así que perfila el 3 % crítico y deja en paz el 97 %. El flujo: claro e idiomático primero, perfilar la carga real, optimizar solo el punto medido, verificar con criterion.

⚔️ Optimiza con sabiduría, no con ansiedad
  1. Reescribe maximo_indexado como iterador, compara su asm (lección 1) y explica por qué el idiomático puede batir al índice pese a parecer más abstracto.
  2. Clasifica en dos columnas —coste cero frente a coste real— estas abstracciones: Vec, Box<dyn Fn>, Arc<T>, Option<u32>, RefCell<T>, un genérico impl Iterator. Justifica cada una.
  3. Enuncia la ley de Amdahl y calcula la mejora máxima del programa si aceleras al doble una función que el perfil sitúa en el 8 % del tiempo total.
  4. Toma un fragmento con un unsafe puesto “por rendimiento”, reescríbelo en seguro y mide con criterion si de verdad había diferencia; interpreta el resultado.
  5. Describe con tus palabras el flujo claro-perfilar-optimizar-verificar y explica por qué escribir claro primero es una decisión de rendimiento y no solo de estilo.