Parte 9 — Ingeniería de software políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Hay una regla que separa a los ingenieros de rendimiento de los aficionados, y McConnell la enuncia sin rodeos en Code Complete: «measure, don't guess» —mide, no adivines—. La intuición humana sobre dónde gasta el tiempo un programa es notoriamente mala; el cuello de botella casi nunca está donde uno cree. Knuth lo dijo antes con su frase más citada y más malentendida: «la optimización prematura es la raíz de todo mal». No significa «no optimices»; significa que optimizar sin datos es tan probable que empeore la legibilidad como que mejore la velocidad. El perfilado es el instrumento que convierte la adivinanza en medición.
Esta clase introduce el perfilado por su puerta más humilde: contar operaciones. El programa suma los enteros de 1 a n y, mientras lo hace, lleva la cuenta de cuántas sumas ejecuta. Ese contador es un perfilador en miniatura: te dice que el trabajo crece linealmente con n —es O(n)—, y esa curva es exactamente lo que un perfilador real revelaría al graficar tiempo contra tamaño de entrada. Hunt y Thomas, en The Pragmatic Programmer, insisten en «estimar el orden del algoritmo» antes de tocar nada: saber si algo es O(n), O(n log n) o O(n²) importa mucho más que cualquier microoptimización, porque la complejidad domina cuando la entrada crece y las constantes no.
Pero la clase también quiere que veas la otra cara: complejidad frente a constantes. Dos algoritmos O(n) pueden diferir diez veces en velocidad real por culpa de constantes ocultas —fallos de caché, asignaciones de memoria, saltos mal predichos—. La complejidad te dice cómo escala; solo el perfilador te dice cuánto cuesta hoy, en esta máquina, con estos datos. Por eso todo lenguaje serio trae perfiladores, y conviene conocer los del ecosistema propio.
Al finalizar, podrás:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Medir antes de optimizar | La intuición sobre el cuello de botella suele fallar |
| 2 | Conteo de operaciones | Estima la complejidad: cuánto trabajo se hace |
| 3 | Complejidad vs. constantes | La curva escala; las constantes deciden el coste hoy |
| 4 | Perfiladores por lenguaje | Cada ecosistema tiene su instrumento |
El perfilado es medir cómo un programa consume tiempo y recursos: cuánto tarda cada función, cuántas veces se llama, cuánta memoria asigna. Hay dos familias. El perfilador de muestreo (sampling) interrumpe el programa muchas veces por segundo y anota qué se estaba ejecutando; es barato y no distorsiona apenas el tiempo, por eso py-spy, perf o async-profiler pueden correr sobre producción. El de instrumentación inserta contadores en cada entrada y salida de función; es preciso pero ralentiza y sesga (el propio instrumento cuesta). El contador ops de esta clase es instrumentación manual llevada al extremo mínimo.
Una operación es la unidad de trabajo que decides contar —una suma, una comparación, un acceso a memoria—. Contarlas da la complejidad: cómo crece el trabajo con el tamaño de la entrada. El cuello de botella (bottleneck) es la porción de código que domina el coste total; la ley de Amdahl formaliza por qué optimizar lo que solo ocupa el 5 % del tiempo no puede darte más de un 5 % de mejora, mientras que atacar el 80 % puede transformarlo todo. McConnell dedica un capítulo entero a esto: primero perfila para hallar el bottleneck, luego optimiza solo ahí, y vuelve a medir para confirmar que ganaste —a veces una «optimización» es más lenta por interacciones con el compilador o la caché.
Un endpoint de tu API tarda 800 ms y el equipo culpa a la base de datos. Antes de reescribir consultas, lo perfilas: py-spy top sobre el proceso en vivo, sin reiniciar nada, revela que el 70 % del tiempo se va en serializar la respuesta a JSON, no en la consulta —que apenas cuesta 40 ms—. Habrías pasado dos días optimizando lo que no importaba. Con el perfilador, cambias el serializador y bajas a 300 ms en una tarde. Este es el patrón profesional: el perfilador reorienta el esfuerzo hacia donde el coste realmente vive. El ejercicio de contar sumas es ese mismo hábito reducido a su núcleo: mirar el trabajo medido, no el imaginado.
n (n >= 1)operaciones=<n> resultado=<1+...+n>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
5 |
operaciones=5 resultado=15 |
1 |
operaciones=1 resultado=1 |
3 |
operaciones=3 resultado=6 |
ops <- 0 ; suma <- 0 ; PARA i de 1 a n: suma+=i ; ops++
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.
python/main.py · python main.pyEl bucle for i in range(1, n + 1) recorre 1..n; en cada vuelta suma i a suma y lleva la cuenta en ops. Al terminar, ops vale exactamente n: acabas de medir que el algoritmo es O(n). Un detalle de rendimiento que Ramalho subraya en Fluent Python: este bucle en Python puro es lento comparado con sum(range(1, n+1)), porque cada iteración pasa por el intérprete. La forma de descubrirlo no es adivinar, sino perfilar: python -m cProfile main.py te da el desglose por función, y py-spy record -o perfil.svg -- python main.py produce un flamegraph —un gráfico donde el ancho de cada barra es el tiempo acumulado, la manera más rápida de ver el cuello de botella de un vistazo.
import sys
n = int(sys.stdin.readline())
ops = 0
suma = 0
for i in range(1, n + 1):
suma += i
ops += 1
print(f"operaciones={ops} resultado={suma}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const n = parseInt(readFileSync(0, "utf8").trim(), 10);
let ops = 0, suma = 0;
for (let i = 1; i <= n; i++) {
suma += i;
ops += 1;
}
console.log(`operaciones=${ops} resultado=${suma}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const n: number = parseInt(readFileSync(0, "utf8").trim(), 10);
let ops = 0, suma = 0;
for (let i = 1; i <= n; i++) {
suma += i;
ops += 1;
}
console.log(`operaciones=${ops} resultado=${suma}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
java/Main.java · java Main.javaEn la JVM el perfilado es especialmente interesante porque el compilador JIT reescribe el código en caliente. Un bucle como este, tras miles de iteraciones, puede ser optimizado —incluso vectorizado o reducido a la fórmula cerrada— por el compilador C2. Por eso medir en Java exige cuidado: hay que «calentar» la JVM. Las herramientas idiomáticas son JFR (Java Flight Recorder, integrado en el JDK) y async-profiler, que producen flamegraphs de bajísimo coste. Para microbenchmarks fiables, Bloch y la comunidad recomiendan JMH, que gestiona el calentamiento y evita que el JIT elimine código «muerto» cuyo resultado no se usa.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine().trim());
long ops = 0, suma = 0;
for (int i = 1; i <= n; i++) {
suma += i;
ops += 1;
}
System.out.println("operaciones=" + ops + " resultado=" + suma);
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
int n = int.Parse(Console.In.ReadToEnd().Trim());
long ops = 0, suma = 0;
for (int i = 1; i <= n; i++) {
suma += i;
ops += 1;
}
Console.WriteLine($"operaciones={ops} resultado={suma}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(line))
ops, suma := 0, 0
for i := 1; i <= n; i++ {
suma += i
ops++
}
fmt.Printf("operaciones=%d resultado=%d\n", ops, suma)
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
rust/main.rs · rustc main.rs -o main && ./mainRust y C son los lenguajes donde las constantes brillan: sin recolector de basura ni intérprete, este bucle compila a instrucciones máquina casi directas, y el optimizador de LLVM probablemente lo reemplace por la fórmula n(n+1)/2. Para medir de verdad en Rust, la herramienta idiomática es criterion, una biblioteca de benchmarking estadístico que ejecuta muchas muestras, descarta valores atípicos y reporta intervalos de confianza —justo el rigor que The Pragmatic Programmer pide para no engañarte con una sola medición ruidosa. Para perfilar el binario ya compilado sirven perf y los flamegraphs, igual que en C.
use std::io::Read;
fn main() {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
let n: i64 = s.trim().parse().unwrap();
let mut ops = 0i64;
let mut suma = 0i64;
for i in 1..=n {
suma += i;
ops += 1;
}
println!("operaciones={ops} resultado={suma}");
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./mainC es donde nació la caja de herramientas clásica: perf (contadores de hardware de Linux: ciclos, fallos de caché, predicciones de salto erradas) y valgrind con su módulo callgrind, que instrumenta cada instrucción para darte un mapa exacto de dónde se gasta el tiempo, a costa de correr mucho más lento. Kernighan y Ritchie ya enseñaban en The C Programming Language a razonar sobre el coste de cada operación; hoy perf stat ./main te muestra esos ciclos reales. Aquí la lección de constantes es directa: el mismo O(n) corre órdenes de magnitud más rápido en C que en Python interpretado.
#include <stdio.h>
int main(void) {
long n;
if (scanf("%ld", &n) != 1) return 1;
long ops = 0, suma = 0;
for (long i = 1; i <= n; i++) {
suma += i;
ops++;
}
printf("operaciones=%ld resultado=%ld\n", ops, suma);
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL: se perfila con EXPLAIN; aquí, conteo y suma.
WITH RECURSIVE r(i) AS (VALUES (1) UNION ALL SELECT i + 1 FROM r WHERE i < 5)
SELECT printf('operaciones=%d resultado=%d', count(*), sum(i)) AS resultado FROM r;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
$n = (int) trim(fgets(STDIN));
$ops = 0;
$suma = 0;
for ($i = 1; $i <= $n; $i++) {
$suma += $i;
$ops++;
}
echo "operaciones=$ops resultado=$suma\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
| Lenguaje | Perfilador idiomático | Notas |
|---|---|---|
| Python | cProfile (instrumentación), py-spy (muestreo) | py-spy corre sobre procesos vivos sin reiniciar |
| JavaScript/TS | perfilador de Node/V8, --prof, Chrome DevTools |
flamegraphs desde el inspector |
| Java | JFR, async-profiler, JMH para microbenchmarks | ojo con el calentamiento del JIT |
| C# | dotnet-trace, dotnet-counters, PerfView | integrados en el SDK de .NET |
| Go | pprof (net/http/pprof, go test -bench) |
CPU, heap y bloqueo, con flamegraphs nativos |
| Rust | criterion (benchmarks), perf + flamegraph | criterion da intervalos de confianza |
| C | perf, valgrind/callgrind, gprof | contadores de hardware reales |
| SQL | EXPLAIN / EXPLAIN ANALYZE |
perfilas el plan de consulta, no un bucle |
| PHP | Xdebug, Blackfire, SPX | Blackfire perfila en producción |
El flamegraph —inventado por Brendan Gregg— es el denominador común: casi todos estos perfiladores exportan a ese formato, donde el eje X es el tiempo acumulado y el Y la pila de llamadas. Aprender a leer uno vale para casi cualquier lenguaje.
El perfilado tiene primos en cada nivel del sistema. En bases de datos, EXPLAIN ANALYZE es un perfilador del plan de ejecución: te dice si la consulta usa un índice o escanea la tabla entera. En el navegador, la pestaña Performance de DevTools perfila render y JavaScript. A escala de sistemas distribuidos, el tracing (OpenTelemetry, Jaeger) es perfilado repartido entre servicios: sigue una petición a través de la red y te dice qué salto costó más. En todos los casos el principio es el mismo que enuncia McConnell: instrumenta, mide, encuentra el cuello de botella, actúa solo ahí, y vuelve a medir.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 152
Detalle en reto.md.
Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 151 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 153 ⏭️