- Un artículo sobre las 5 reglas de programación de Rob Pike de 1989
- Regla 1: no asumas dónde pasará la mayor parte del tiempo un programa; los cuellos de botella pueden aparecer de forma inesperada. Evita los hacks de velocidad hasta que se demuestre que existe un cuello de botella.
- Regla 2: siempre mide antes de ajustar por velocidad. Optimiza solo cuando una parte del código tenga un impacto significativo en el resto.
- Regla 3: los algoritmos complejos son lentos cuando
nes pequeño. Ese es el caso la mayoría de las veces. Usa algoritmos complejos solo cuandonsea grande con frecuencia, y aun así aplica primero la regla 2. - Regla 4: los algoritmos simples y las estructuras de datos simples son deseables. Son menos propensos a errores y más fáciles de implementar que las cosas complejas.
- Regla 5: la estructura de datos correcta es decisiva en la programación. Si los datos están bien organizados, el algoritmo se volverá evidente.
- Las reglas 1 y 2 de Pike reflejan el aforismo de Tony Hoare: "la optimización prematura es la raíz de todos los males".
- Ken Thompson reformuló las reglas 3 y 4 de Pike como: "cuando tengas dudas, usa la fuerza bruta".
- Las reglas 3 y 4 encarnan la filosofía de diseño KISS (Keep It Simple, Stupid).
- La regla 5 coincide con una afirmación de Fred Brooks en 'The Mythical Man-Month', a menudo abreviada como: "escribe código tonto que use objetos inteligentes".
1 comentarios
Opiniones de Hacker News
Estoy totalmente de acuerdo con eso de que “los datos mandan”.
Por eso las entrevistas tipo LeetCode siempre me parecieron raras. Suelen enfocarse en algoritmos, pero en la práctica muchas veces no deberías abordar las cosas así desde el principio, y las estructuras de datos deberían estar más al centro.
Claro que, si no sabes nada de algoritmos, quizá no detectes casos excepcionales o momentos en que, por alguna razón concreta, necesitas apoyarte en un algoritmo específico. Aun así, los algoritmos se pueden enseñar en relativamente poco tiempo, mientras que desarrollar intuición sobre qué estructura de datos usar parece resultarle más difícil a la gente.
En ese momento el ambiente cambia a “ah, entró un verdadero ingeniero senior”, la conversación sobre el problema técnico se abre más y disminuye la actitud de intentar demostrar “si sabe programar”.
En cambio, los equipos con los que más me costó generar buenos cambios, cumplir hitos y colaborar fueron aquellos donde no había nadie que definiera bien las estructuras de datos y la arquitectura del código. Parece que se ha vuelto común gente acostumbrada a que el framework lo haga todo y, si no funciona, algún plugin o middleware hecho por alguien más inteligente lo resolverá.
Un ingeniero que evita las estructuras de datos se está disparando en el pie y renuncia a una de las herramientas más útiles, así que sus límites se notan en el día a día.
Por ejemplo, el código puede usarse mucho para encontrar el camino más largo en un grafo acíclico dirigido (DAG) con pesos, pero la clave estaba en darse cuenta de que el problema podía representarse como un DAG con pesos. Si no ves eso, todavía puedes resolverlo, pero la solución será mucho más lenta y compleja.
El entrevistador no te va a decir de entrada que debes usar una cola de prioridad, una matriz de adyacencia o un trie. Si te trabas puede darte una pista, pero guiar demasiado difícilmente se vea como una señal fuerte para contratar.
Sobre la frase “los algoritmos elegantes son lentos cuando n es pequeño, y n suele ser pequeño”, algo que sentí en un proyecto reciente es que una n grande puede ser mucho más grande de lo que uno cree.
Es fácil pensar “tengo que hacer 100.000 operaciones, así que sí o sí debo optimizar”, pero las computadoras son rápidas, y 100.000 multiplicaciones suelen ser tan rápidas que quizá no vale la pena pensarlo demasiado.
No digo que no haya que pensar en absoluto, pero a menudo sorprende lo absurdamente rápido que es el hardware moderno.
He visto incidentes en producción causados por código que accidentalmente era de tiempo cuadrático, y aunque el 99% de los usuarios use siempre n pequeña, algunos usuarios se encontrarán con n grandes con frecuencia y sufrirán una app muy lenta.
En la mayoría de los casos prefiero elegir un algoritmo mejor que cuadrático, aunque sea un poco más lento en el caso común y un poco más complejo de implementar. Las rutas lentas frecuentes se optimizan, pero las rutas lentas raras el desarrollador no las recorre directamente y se le pasan, o revientan en producción.
Claro que, si el algoritmo es demasiado complejo, quizá elegiría una implementación cuadrática simple, pero mi valor por defecto es apuntar a algo subcuadrático siempre que sea posible. También escribí sobre esto: https://kevincox.ca/2023/05/09/less-than-quadratic/
Por eso quizá tenía más sentido hace 40 años, cuando la CPU no era tan rápida comparada con la memoria y en hardware de consumo no se prestaba tanta atención a los fallos de predicción de ramas.
En cada entrevista el hiring manager lo quiere, pero pasa que un principiante de LeetCode, que todavía no ha sufrido las heridas de producción, se niega a tomar esa decisión.
https://www.frankmcsherry.org/assets/COST.pdf
Pensando en el aumento de rendimiento de las computadoras durante los últimos 20 años, parece bastante razonable subir ese umbral a 100.000.
El famoso aforismo “la optimización prematura es la raíz de todos los males” en realidad no viene de Tony Hoare, sino de Donald Knuth, y a menudo se usa sin contexto como si se opusiera a la optimización en general.
La frase completa es: “Debemos olvidarnos de las pequeñas eficiencias, digamos, el 97% de las veces. La optimización prematura es la raíz de todos los males. Sin embargo, no debemos dejar pasar las oportunidades en ese 3% crítico”.
La idea es dedicar tiempo a optimizar donde sí hay impacto.
Parece posible que Tony lo haya dicho primero y Knuth lo haya pulido y publicado. Siempre es bueno incluir la cita larga que aporta el contexto necesario.
Programar entonces era muy distinto a programar hoy. En ese momento, “optimización prematura” no era tanto “usemos sin más una biblioteca popular y escalable”, sino algo más cercano a “usemos un algoritmo incomprensible de manipulación de bits que solo funciona en este hardware”.
Ese sentido ya está incluido en “la optimización prematura es la raíz de todos los males”; el aforismo no dice que “la optimización sea la raíz de todos los males”.
En entrevistas de estructuras de datos y algoritmos en empresas, vi incontables desarrolladores frontend decir que bubble sort era la mejor opción. No hace falta que lo deriven en el momento; alcanza con conocer algunos métodos y poder decir cuál sería una buena elección para el problema.
Si uno lleva “no optimices prematuramente” tan al extremo que ni siquiera conoce métodos eficientes, ¿cómo va a saber qué partes son importantes?
La frase “las estructuras de datos son lo central” es doblemente importante en las bases de datos.
Quienes usan la DB como un simple contenedor tonto de bits o como un reflejo 1:1 de definiciones de objetos suelen sorprenderse cuando la DB se lo toma personal y arruina el rendimiento.
Si vuelvo a ver otro esquema de DB generado por un ORM, será demasiado pronto.
El problema es que algunos, o muchos, desarrolladores no saben SQL ni tienen el conocimiento de DB necesario para usar un ORM.
Un ORM es una abstracción bastante permeable: hay que saber qué hay debajo. Si se entiende eso, se pueden crear esquemas decentes con la mayoría de los ORM.
Para organizar bien las estructuras de datos y mantenerlas así aunque cambie el diseño, hay que separar datos y código a nivel organizacional.
El diseño del esquema de la DB, los casos de uso y el mapeo entre ambos deben separarse del resto de la implementación, y ese grupo también debería escribir cosas como verificaciones de integridad. Si la estructura de la organización no separa datos y código, es difícil separar el código de los datos.
Mi regla adicional es que los pequeños desperdicios de rendimiento, aunque cada uno parezca poca cosa, se acumulan y terminan haciendo lento al programa.
Si no afecta la complejidad, la legibilidad, la mantenibilidad ni el costo de implementación, no deberíamos simplemente dejar rendimiento sobre la mesa. Si todo lo demás es casi igual, no está bien elegir la opción más lenta entre dos alternativas.
Además, si asumimos que n es pequeño, casi cualquier cosa funciona. Pero si se escribe código que anda bien con n menor o igual a 100 y se rompe con 10000 o más, por ejemplo algo O(n²), entonces simplemente habría que imponer un límite. Si la suposición de n pequeño se rompe, es mejor fallar fuerte con un error que recibir una factura explosiva de AWS o terminar con un programa colgado.
Muchas de estas guías, al final, se reducen a estrategias para evitar el sobrediseño.
En mi experiencia, la optimización prematura es una de las trampas más caras. Si se rodea demasiado pronto un problema potencial, esa suposición no se valida, y el siguiente equipo tiene que construir soluciones costosas para lidiar con complejidad innecesaria.
El enfoque que aprendí es este: la optimización depende de estimaciones, y las estimaciones tempranas suelen estar equivocadas.
También descubrí que, para evitar que la gente escriba código excesivamente complejo, es bastante importante gestionar el ego y entender la psicología.
La sobreproducción suele considerarse el peor desperdicio, porque no solo produce algo que no se necesita, sino que además consume esfuerzo que podría haberse usado en lo que realmente hacía falta. El sobrediseño es parecido.
Por ejemplo, empezar con una arquitectura de microservicios aunque solo haya 100 usuarios, argumentando que si algún día llegan a ser 1 millón será difícil rediseñar el monolito.
Por eso, primero habría que abordar por qué el código se vuelve menos maleable con el tiempo.
En general son buenas reglas, pero en la práctica la regla 1 no se cumple tal cual.
Al empezar, se necesita una hipótesis sobre qué será el cuello de botella. No siempre es posible implementar XYZ y luego medir qué está lento y corregirlo. X, Y y Z pueden estar conectados, de modo que para hacer que Y sea rápido haya que construir X y Z de cierta manera, y a veces ya se sabe que Y será el cuello de botella.
Aunque más adelante midas y descubras qué está lento, tienes que apostar por un enfoque para hacerlo más rápido. Mientras más informada sea la apuesta, mejor.
Los buenos programadores miden, pero también pueden prever qué será lento, qué tendrá muchos bugs y qué usará mucha memoria, por lo que iteran menos. Plantear como regla que no se puede predecir el comportamiento de rendimiento equivale a ignorar la experiencia y las habilidades que acumulan los buenos programadores.
Porque el proceso de seguir la regla 1 es precisamente la mejor forma de obtener la experiencia y el contexto empírico necesarios para desarrollar una buena intuición sobre los cuellos de botella.
Si la predicción de velocidad resulta equivocada, terminas cargando con código innecesariamente complejo durante toda la vida del proyecto.
La gente se equivoca con frecuencia sobre la velocidad de los algoritmos. Si la computadora pasa el 99% del tiempo trayendo n desde el servidor de BD, muchas veces O(n) y O(n²) parecen iguales en tiempo real.
A veces un algoritmo escrito en C puede ser más lento que el código equivalente en Python, quizá porque el compilador de bytecode hizo algo inteligente.
He trabajado mucho haciendo más rápido código legacy, y por lo general es mucho más fácil de lo que uno cree, y es lento por razones que no eran obvias para el autor original. En la práctica, muchas veces es lento porque la base de código se volvió tan compleja que el autor original ya no podía razonar sobre ella. Para mí es fácil depurarlo porque tengo un caso concreto de “demasiado lento”, así que puedo ejecutarlo y observar dónde se vuelve lento.
Si estás haciendo un videojuego con muchos objetos físicos y, por experiencia, sabes con certeza que la detección de colisiones será un gran problema, diseñar el juego y el sistema alrededor de eso no es un hack de velocidad.
Si trabajas en algo donde sabes que el rendimiento será una preocupación importante, por supuesto que debes medir. No para confirmar si es una preocupación, sino para verificar qué tan bien la estás manejando.
Si estás creando un sistema nuevo con requisitos nuevos, creo que muchas veces está bien simplemente empezar. Construyes, pruebas y mides, tiras o refactorizas, y repites.
Tomando Rust como ejemplo, empezó con un lenguaje borrador y un compilador hecho en OCaml, y fue iterando. Aunque supieran que algún día podrían pasar de OCaml a ser self-hosting, no estoy seguro de que eso hubiera marcado una gran diferencia.
Si no hay usuarios, incluso una función que tarda horas sigue siendo suficientemente rápida en comparación con una función que, al optimizarse, pasa a tardar milisegundos. No sé si haya alguien que haya demostrado poder hacer esas predicciones con precisión.
Como objeción a la regla 5, los algoritmos complejos sobre datos simples pueden aportar grandes mejoras de rendimiento, eliminar obstáculos e incluso simplificar las cosas.
Por ejemplo, si en vez de un objeto BinaryTree haces búsqueda binaria sobre un arreglo ordenado, las fusiones se simplifican a concatenar y luego ordenar; al no haber punteros, la serialización es fácil y, según el caso, ni siquiera hace falta serializar. El arreglo puede estar en disco, en memoria o en ambos mediante mmap; puede manejar datos más grandes que la RAM, y también permite un arranque en frío apuntando solo al archivo o al mapeo y ejecutando de inmediato. Además tiene propiedades cache-oblivious.
La codificación Huffman es otro ejemplo. En la universidad normalmente se aprende como un algoritmo basado en árboles con complejidad O(n log n), pero yo no sabía que había una forma de construir un árbol de Huffman in-place, basado en arreglos, en tiempo lineal.
Claro, el 99% del tiempo uno construye microservicios de backend y usa estructuras de datos de colecciones estándar. Pero si en el trabajo hago tareas de big data, preferiría con mucho procesarlas en una sola máquina local con discos grandes antes que adoptar la familia de tecnologías tipo MapReduce que esté de moda en ese momento.
Creo que Rob Pike primero perfilaría el código y luego vería si el código sofisticado o la estructura de datos alternativa realmente es más rápida.
Leí esto por primera vez en cat-v hace más de 10 años, y tuvo un impacto indeleble en mi forma de abordar y pensar el diseño y la complejidad.
http://doc.cat-v.org/bell_labs/pikestyle
No entiendo cómo de la regla original “las estructuras de datos son lo central” se reduce a “escribe código tonto que use objetos inteligentes”.
La expresión “smart objects” me pareció muy mala; la regla original, aunque sea más larga, es mucho mejor.