Artículos en profundidad sobre la tecnología que da forma al futuro.

Las reglas de programación de Rob Pike siguen vigentes

Rob Pike escribió cinco reglas de programación en 1989. Hoy son más relevantes que nunca, sobre todo las que seguimos ignorando.

Ficha antigua clavada junto a un escritorio moderno y desordenado bajo una sola lámpara

En 1989, Rob Pike, que después co-crearía Go, UTF-8 y Plan 9, escribió cinco reglas de programación. Son lo bastante cortas para caber en una ficha y lo bastante profundas como para que la comunidad lleve 37 años discutiéndolas. La mayoría de desarrolladores ha visto alguna citada en blogs o charlas, pero pocos las han interiorizado de verdad, y es una pena, porque se ahorrarían muchísimo esfuerzo perdido.

Las reglas son engañosamente sencillas. No tratan de sintaxis ni de patrones de arquitectura. Hablan de dónde los programadores pierden tiempo de forma sistemática y de cómo dejar de hacerlo.

Las cinco reglas

Vamos a enunciarlas tal cual antes de diseccionarlas:

  1. No puedes saber dónde va a consumir tiempo un programa. Los cuellos de botella aparecen en sitios sorprendentes, así que no intentes adivinarlos ni metas un parche de velocidad hasta haber demostrado que ahí está el cuello de botella.
  2. Mide. No optimices la velocidad hasta haber medido, y ni siquiera entonces, a menos que una parte del código domine por completo al resto.
  3. Los algoritmos sofisticados son lentos cuando n es pequeño, y n suele ser pequeño. Tienen constantes grandes. Mientras no sepas que n va a ser frecuentemente grande, no te compliques.
  4. Los algoritmos sofisticados tienen más bugs que los sencillos y son mucho más difíciles de implementar. Usa algoritmos simples y estructuras de datos simples.
  5. Los datos dominan. Si has elegido las estructuras de datos adecuadas y has organizado bien las cosas, los algoritmos casi siempre resultan evidentes. Son las estructuras de datos, no los algoritmos, el centro de la programación.

Las reglas 1 y 2 tratan sobre optimización. Las 3 y 4, sobre complejidad. La 5, sobre diseño. En conjunto forman una filosofía que, en el fondo, es una cuestión de humildad: admitir que nuestra intuición sobre el rendimiento falla, que la complejidad tiene costes que subestimamos y que las buenas estructuras de datos importan más que el código ingenioso.

Regla 1: no sabes dónde está el cuello de botella

Es la regla que los desarrolladores más confiadamente se saltan. «Sé que esta función es lenta porque tiene un bucle anidado». «Debería usar un hash map aquí, porque las búsquedas son O(1)». «Voy a preasignar este array, porque reservar memoria es caro». Suenan razonables. Muy a menudo están equivocados.

He perfilado suficientes sistemas en producción como para tener una colección de casos en los que el cuello de botella intuitivo no era el real. Un sistema en el que todo el mundo asumía que la base de datos era el problema, pero el profiling mostró que la serialización JSON consumía el 60 % del tiempo de cada petición. Una pipeline de datos en la que la multiplicación de matrices, que parecía lo más costoso, ocupaba el 5 % del tiempo de ejecución, mientras que el parseo de CSV se llevaba el 70 %. Una aplicación web cuyo equipo pasó meses optimizando consultas a la base de datos, cuando el verdadero cuello de botella era la resolución DNS en cada petición HTTP saliente.

El cerebro humano es un pésimo profiler. Sobrevaloramos las operaciones que conceptualmente parecen caras (consultas a bases de datos, llamadas de red) y subestimamos las que parecen baratas (concatenar cadenas, parsear JSON, reservar memoria). El hardware moderno lo empeora: las cachés de CPU, la predicción de saltos y la ejecución fuera de orden hacen que la relación entre la complejidad del código y el tiempo de ejecución sea profundamente contraintuitiva.

Regla 2: mide primero, optimiza después

Es la consecuencia práctica de la regla 1. No optimices por intuición. Perfila, encuentra el punto caliente real y optimiza solo eso.

La segunda mitad de esta regla, «no lo hagas a menos que una parte del código domine al resto», es igual de importante y se cita menos. Si tu profiler muestra que el tiempo de ejecución está repartido de forma uniforme entre 20 funciones, cada una con un 5 %, no hay un único cuello de botella que atacar. Hacer una función 2 veces más rápida ahorra solo el 2,5 % del total. Rara vez compensa la complejidad añadida. Lo que necesitas es un enfoque fundamentalmente distinto, en lugar de optimizar funciones sueltas.

# Profile first. Always.
# Python
python -m cProfile -s cumulative my_script.py
# Node.js
node --prof app.js
node --prof-process isolate-*.log > profile.txt
# Go
go test -bench=. -cpuprofile=cpu.prof
go tool pprof cpu.prof
# Rust
cargo flamegraph
# General (Linux)
perf record ./my_program && perf report
# The flamegraph (brendangregg.com/flamegraphs) is the most
# informative visualization. It shows you exactly where time
# is spent, in a format that makes bottlenecks visually obvious.

Regla 3: los algoritmos sofisticados son lentos cuando N es pequeño

Esta es la regla que la formación en informática suele presentar al revés. Enseñamos que O(n log n) es mejor que O(n²), y asintóticamente es cierto. Pero para n = 20, un insertion sort O(n²) bien implementado es más rápido que un merge sort O(n log n), por los factores constantes, el comportamiento de la caché y la sobrecarga.

Los ejemplos del mundo real abundan. Una búsqueda lineal en un array ordenado de 50 elementos es más rápida que una búsqueda binaria, porque la lineal tiene un comportamiento de caché perfecto y ninguna predicción de saltos fallida. Una lista enlazada simple supera a un árbol binario equilibrado para colecciones de menos de ~100 elementos, porque recorrer punteros en un árbol destruye la localidad de caché. Los hash maps tienen una búsqueda amortizada O(1), pero la constante es tan alta que, para colecciones de menos de ~30-50 elementos, una búsqueda lineal en un array es más rápida.

Las bibliotecas estándar lo saben. El sorted() de Python usa Timsort, que recurre a insertion sort para subsecuencias pequeñas. El std::sort de C++ cambia a insertion sort por debajo de un umbral (normalmente entre 16 y 32 elementos). El sort_unstable de Rust combina quicksort con insertion sort. El algoritmo «sofisticado» solo se usa cuando n es realmente lo bastante grande para que compense.

La lección general: conoce tu n. Si estás eligiendo entre un O(n²) sencillo y un O(n log n) complejo, pregúntate cuánto valdrá n en la práctica. Si es menor de unos pocos cientos, el algoritmo simple casi seguro basta, y además será más fácil de escribir, depurar y mantener.

Regla 4: lo simple es mejor que lo ingenioso

La regla 4 extiende la 3 más allá del rendimiento. Los algoritmos sofisticados no solo son más lentos con n pequeño, sino que también tienen más bugs. Un árbol rojo-negro tiene más casos límite que un array ordenado. Una estructura de datos concurrente sin locks tiene modos de fallo más sutiles que una protegida con un mutex. Un asignador de memoria personalizado tiene más formas de corromper la memoria que el asignador del sistema.

He visto equipos dedicar semanas a implementar y depurar una caché LRU personalizada con operaciones O(1), cuando un array acotado con desalojo lineal se habría escrito en una tarde, funcionado a la primera y sido lo bastante rápido para su carga de trabajo (que tenía como mucho unos cientos de entradas).

El coste de la complejidad no está solo en la implementación inicial. Está en cada desarrollador futuro que tenga que entender, modificar y depurar el código. Un algoritmo simple que todo el equipo entiende vale más que uno ingenioso que solo el autor original sabe mantener. Y ese autor, seis meses después, es prácticamente otra persona que también ha olvidado cómo funciona.

Depurar es dos veces más difícil que escribir el código en primer lugar. Por tanto, si escribes el código lo más ingeniosamente posible, por definición no eres lo bastante listo para depurarlo. — Brian Kernighan

Regla 5: los datos dominan

Es la regla más importante de Pike y la que más pasan por alto los desarrolladores centrados en algoritmos y patrones de diseño. La tesis: si tus estructuras de datos son correctas, los algoritmos se deducen de forma natural. Si son incorrectas, ninguna cantidad de ingenio algorítmico te salvará.

Fred Brooks dijo algo parecido: «Muéstrame tus diagramas de flujo y oculta tus tablas, y seguiré desconcertado. Muéstrame tus tablas, y normalmente no necesitaré tus diagramas de flujo». Linus Torvalds lo recogió así: «Los malos programadores se preocupan por el código. Los buenos programadores se preocupan por las estructuras de datos y sus relaciones».

Este principio aparece constantemente en la práctica. Una base de código que guarda los permisos de usuario como una lista plana de cadenas acumulará lógica de comprobación compleja y propensa a errores, dispersa por todo el código. Reestructura los datos como una jerarquía de roles, y la lógica de comprobación se vuelve trivial. Un sistema que guarda eventos como blobs JSON necesitará parseo y validación complejos en cada consumidor. Estructura los eventos como registros tipados con esquemas explícitos, y los consumidores se simplifican drásticamente.

# Bad data structure → complex algorithm
class OrderSystem:
def __init__(self):
self.orders = []  # flat list of all orders
def get_user_orders(self, user_id):
return [o for o in self.orders if o.user_id == user_id]  # O(n)
def get_pending_orders(self):
return [o for o in self.orders if o.status == 'pending']  # O(n)
def get_user_pending_orders(self, user_id):
return [o for o in self.orders
if o.user_id == user_id and o.status == 'pending']  # O(n)
# Better data structure → algorithms become obvious
class OrderSystem:
def __init__(self):
self.orders_by_user = {}      # user_id → [orders]
self.orders_by_status = {}    # status → [orders]
def get_user_orders(self, user_id):
return self.orders_by_user.get(user_id, [])  # O(1)
def get_pending_orders(self):
return self.orders_by_status.get('pending', [])  # O(1)
# The right data structure makes the code self-evident.
# You don't need to think about algorithms at all.

Cómo Go encarna estas reglas

Es difícil mirar las reglas de Pike sin ver en embrión la filosofía de diseño de Go. Go, que Pike co-creó 20 años después de escribir estas reglas, es un lenguaje que favorece sistemáticamente la simplicidad frente al ingenio.

  • Sin genéricos (al principio): obliga a usar estructuras de datos simples. (Los genéricos llegaron en Go 1.18, pero solo tras años de resistencia, hasta encontrar un diseño lo bastante simple.)
  • Sin sobrecarga de operadores: el código significa lo que parece.
  • Sin conversiones de tipo implícitas: explicitud antes que ingenio.
  • Sin excepciones: maneja los errores donde se producen.
  • Algoritmos de la biblioteca estándar mínimos: usa slices y mapas, no estructuras de datos sofisticadas.
  • Profiling integrado (pprof): mide, no adivines.

Go suele ser criticado de «aburrido» por desarrolladores que prefieren lenguajes más expresivos. Y ahí está precisamente el punto. Las reglas de Pike son una receta para código aburrido: código sencillo, medible y construido sobre buenas estructuras de datos en lugar de algoritmos ingeniosos. Go es lo que pasa cuando conviertes esa receta en un lenguaje.

Dónde no aplican las reglas

Ningún conjunto de reglas es universal, y las de Pike tienen excepciones legítimas. Los sistemas críticos para el rendimiento (motores de juegos, internals de bases de datos, compiladores) a veces necesitan algoritmos sofisticados porque su n es realmente grande. El código de infraestructura que se ejecuta millones de veces por segundo justifica optimizaciones que el código de aplicación no. Y a veces el algoritmo «simple» tiene una complejidad O(n³) que resulta inaceptable incluso para n modestos.

Las reglas son heurísticas, no leyes. Su valor está en corregir el sesgo habitual: los desarrolladores tienden a optimizar demasiado pronto, a usar algoritmos demasiado complejos y a pensar demasiado en el código y muy poco en las estructuras de datos. Las reglas de Pike empujan contra esas tendencias. Si te encuentras en la rara situación en la que se aplica el sesgo contrario, en la que de verdad necesitas más complejidad y no menos, adelante, complícate. Pero mide primero.

Treinta y siete años después de que Pike las escribiera, estas reglas siguen siendo de los mejores consejos de programación jamás publicados. No porque sean sorprendentes: la mayoría de desarrolladores con experiencia, al leerlas, piensan «sí, es obvio». El valor está en tenerlas enunciadas con suficiente claridad como para aplicarlas de forma consistente. La próxima vez que vayas a usar un árbol rojo-negro, un asignador personalizado o una «optimización» que no has perfilado, recuerda: mide primero, mantenlo simple y acierta con las estructuras de datos.