2 puntos por GN⁺ 2023-11-02 | 1 comentarios | Compartir por WhatsApp
  • 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 n es pequeño. Ese es el caso la mayoría de las veces. Usa algoritmos complejos solo cuando n sea 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

 
GN⁺ 2023-11-02
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.

    • Por mi experiencia, también estoy de acuerdo. En entrevistas, cuando pasas de una verificación de algoritmos tipo FizzBuzz y empiezas directamente a hablar de estructuras de datos, arquitectura y cómo se corresponden con el dominio, he visto que el entrevistador te respeta mucho más.
      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.
    • Al ayudar a mi sobrino a prepararse para competencias de programación competitiva, vi que en la mayoría de los problemas una gran parte de la solución era transformar los datos en una estructura de datos adecuada.
      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.
    • Los problemas comunes de LeetCode en realidad también se enfocan en estructuras de datos. Porque el candidato debe tener en la cabeza una lista de estructuras de datos a la cual recurrir cuando hace pattern matching entre el problema y la solución.
      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.
    • “Muéstrame los diagramas de flujo y ocúltame las tablas, y seguiré confundido. Muéstrame las tablas y no necesitaré ver los diagramas de flujo. Se volverá claro por sí solo”.
    • Si hay que elegir una de las dos, creo que igual necesitas una noción general de la otra. Si no tienes idea de cómo acceder a los datos, es difícil saber qué estructura de datos deberías usar.
  • 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.

    • Me cuesta estar muy de acuerdo con esto. Los algoritmos de tiempo cuadrático son de esos que pueden morderte cuando menos lo esperas.
      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/
    • La jerarquía de memoria también influye en esto. Muchos algoritmos elegantes tienen mala localidad de referencia y agregan ramificaciones.
      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 problemas de entrevista de LeetCode sigo viendo recorridos de listas de 100.000 elementos varias veces. Puede que no sea lo óptimo, pero en términos de tiempo real en producción, recorrer 100.000 elementos no es nada comparado con la llamada de red que se hace justo después.
      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.
    • La referencia clásica sobre este tema es Scalability! But at what COST?
      https://www.frankmcsherry.org/assets/COST.pdf
    • Cuando empecé mi primer trabajo serio de programación en una empresa de videojuegos a principios de los 2000, el director técnico me aconsejó: “si la cantidad de elementos que manejas ronda los 10.000, no optimices”.
      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.

    • Knuth dice que fue Hoare quien lo dijo, y Hoare dice que fue Knuth, así que es cuestión de a quién creerle. Probablemente lo mejor sea atribuírselo a ambos.
      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.
    • También se suele olvidar que esa cita es de fines de los años 70. Hace casi 50 años.
      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”.
    • No creo que la cita larga agregue un contexto especialmente significativo. Si ya mediste y encontraste el 3% importante, entonces eso ya no es prematuro.
      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”.
    • Mucha gente toma esta frase como doctrina y ni siquiera aprende métodos eficientes.
      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?
    • En este contexto, no parece que se haya usado con el sentido de oponerse a la optimización en general.
  • 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.

    • Creo que la mayoría de los ORM generan el esquema que uno les pide. Usar un ORM no produce automáticamente un diseño de base de datos peor que hacerlo a mano.
      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.
    • A esto también se puede sumar la ley de Conway: “las organizaciones que diseñan sistemas terminan produciendo diseños que replican la estructura de comunicación de la organización”.
      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.
    • Ganan los procedimientos almacenados.
  • 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.

    • Aquí aplican las reglas 1 y 2.
  • 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.

    • Suelo decirlo así: “resuelve el problema que tienes, no el problema que crees tener”.
    • Este concepto también se relaciona con la identificación de desperdicios en lean y Six Sigma.
      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.
    • Si vamos un nivel más profundo, el sobrediseño surge al pensar que quizá más adelante se necesite complejidad y que entonces ampliar el sistema será más difícil o riesgoso.
      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 el manejo de errores, conviene no hacerse el elegante y fallar temprano y de forma simple.
  • 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.

    • La regla 1 es una ley inamovible para quienes no creen en ella, y una guía flexible para quienes sí creen.
      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.
    • Un algoritmo que parece que será lento se puede verificar con una implementación spike. Normalmente, los algoritmos lentos son fáciles de implementar y probar.
      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.
    • No parece ser una reacción al texto completo original. Ahí se dice que no hay que meter hacks de velocidad hasta saber dónde está el cuello de botella. Es distinto de la situación descrita.
      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.
    • Me gustaría ver un ejemplo concreto. En la mayoría de los casos, dudo que realmente haga una diferencia.
      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 los desarrolladores pudieran predecir tan bien qué será lento, ¿no debería ser del 100% la tasa de éxito de las startups lideradas por desarrolladores?
      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.

    • No considero que la búsqueda binaria sea un algoritmo sofisticado. Las funciones de ordenamiento modernas son lo sofisticado, y pueden tener bugs sutiles, así que un desarrollador promedio no debería implementarlas por su cuenta. Incluso quicksort tiene trampas.
      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.
    • Esto no me parece una objeción. Desde el punto de vista del consejo de Pike, “búsqueda binaria en un arreglo ordenado” y “objeto BinaryTree” son simplemente distintas implementaciones de la misma estructura de datos.
    • No hay que olvidar que el desarrollador es el recurso más caro el 99% del tiempo. La mantenibilidad y la velocidad de salida al mercado suelen ser mucho más importantes.
  • 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.

    • Creo que Rob Pike también estaría de acuerdo en que “smart objects” es una forma equivocada de pensar: https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
    • Llevar la lógica “inteligente” a un nivel más alto facilita entenderla, probarla y cambiarla. Me parece que es mucho más difícil hacer que los smart objects sean cohesivos entre sí.
    • Puede entenderse como: escribe código que surja naturalmente de objetos bien estructurados.