Subir los if, bajar los for
(matklad.github.io)- Subir las sentencias
ifdesde dentro de una función hacia el sitio donde se la llama ayuda a reducir la complejidad del código - Concentrar la verificación de condiciones y el manejo de ramas en un solo lugar permite detectar con facilidad duplicaciones y comprobaciones de ramas innecesarias
- Usar el refactor de disolver enums ayuda a evitar que la misma condición se disperse por distintas partes del código
- Los bucles
forbasados en operaciones por lotes son eficaces para mejorar el rendimiento y optimizar tareas repetitivas - Combinar el patrón de subir los
ify bajar losforpermite aumentar al mismo tiempo la legibilidad y la eficiencia del código
Una nota breve sobre dos reglas relacionadas
- Cuando existe una condición
ifdentro de una función, se recomienda pensar si puede moverse al sitio donde la función es llamada - Como en el ejemplo, en lugar de comprobar una precondition (precondición) dentro de la función, es preferible delegar esa validación al punto de llamada o hacer que el tipo (o un
assert) garantice la precondición - La estrategia de subir (Push up) las verificaciones de precondición afecta al código en general y produce el efecto de reducir la cantidad de comprobaciones innecesarias
Concentración del flujo de control y de las condiciones
- El flujo de control y las sentencias
ifson una de las principales causas de complejidad y errores en el código - Es útil concentrar las condiciones en niveles superiores, como el punto de llamada, de modo que el manejo de ramas quede reunido en una sola función y el trabajo real se delegue a subrutinas lineales (straight-line)
- Cuando las ramas y el flujo de control se reúnen en un solo lugar, resulta más fácil identificar ramas duplicadas y condiciones innecesarias
Ejemplo:
- Cuando hay
ifanidados dentro de la función f, es más fácil reconocer código muerto (Dead Branch) - Si las ramas están dispersas a través de varias funciones (g, h), esto se vuelve más difícil de detectar
Refactor de disolver enums (Dissolving enum Refactor)
- Si el código encapsula la misma rama condicional en un
enumu otra estructura, se puede subir la condición a un nivel superior para separar con mayor claridad las ramas y el trabajo - Aplicar este enfoque evita que la misma condición aparezca repetida varias veces en el código
Ejemplo:
- Una situación en la que la misma condición de rama está expresada por separado en las funciones f, g y en el
enumE - Puede simplificarse todo el código con una sola rama condicional superior
Pensamiento orientado a datos (Data Oriented Thinking) y operaciones por lotes
- La mayoría de los programas funcionan con múltiples objetos (entidades). El rendimiento en la ruta crítica (Hot Path) suele depender de procesar muchos objetos
- Es recomendable introducir el concepto de lote (batch) para que las operaciones sobre conjuntos de objetos sean la base, y tratar las operaciones sobre un solo objeto como un caso especial
Ejemplo:
-
Tener como base una función de procesamiento por lotes, como
frobnicate_batch(walruses) -
Y convertir el tratamiento de objetos individuales en un caso especial procesado mediante un bucle
for -
Este enfoque cumple un papel importante en la optimización del rendimiento, ya que en trabajos masivos reduce el costo de arranque y aumenta la flexibilidad del orden de procesamiento
-
También permite aprovechar operaciones SIMD (como struct-of-array), de modo que se puedan procesar ciertos campos en bloque antes de continuar con el trabajo completo
Casos prácticos y patrón recomendado
- Como en la multiplicación de polinomios basada en FFT, se puede maximizar el rendimiento al permitir operaciones simultáneas en múltiples puntos
- La regla de subir las condiciones y bajar los bucles puede aplicarse en paralelo
Ejemplo:
- En lugar de comprobar la misma condición una y otra vez dentro de un bucle, sacar la condición fuera del bucle reduce las ramas dentro de la iteración y facilita la optimización y vectorización
- Este enfoque garantiza una alta eficiencia en planos de datos de sistemas a gran escala, como el diseño de TigerBeetle
Conclusión
- Al combinar el patrón de mover los
if(condiciones) hacia arriba —al punto de llamada o de control— y losfor(iteraciones) hacia abajo —a la capa de operación o procesamiento de datos—, se puede mejorar la legibilidad, la eficiencia y el rendimiento del código - Pensar desde la perspectiva de un espacio vectorial abstracto (operaciones sobre conjuntos) es una mejor herramienta para resolver problemas que el manejo repetitivo de ramas
- En resumen: ¡los
ifhacia arriba, losforhacia abajo!
1 comentarios
Opiniones en Hacker News
ifen cada llamada? Por ejemplo, ¿de verdad podrías decir que hay que mover eseifen funciones comogetaddrinfooEnterCriticalSection? Creo que esta transformación solo vale la pena considerarla cuando la función se llama desde un par de lugares y cuando esa decisión está fuera de la responsabilidad de la función. Una manera de hacerlo es delegar en una función helper que solo ejecute la condición. Y cuando necesites mover la condición fuera del bucle, puedes hacer que el llamador use directamente el helper de condición de más bajo nivel. Pero en el fondo este tipo de discusión gira alrededor de la “optimización”. Muchas veces la optimización choca con un mejor diseño del programa. Puede ser mejor diseño que el llamador no necesite conocer la condición. Este dilema aparece mucho también en OOP. Una decisión representada por unifa veces en realidad se resuelve mediante despacho de métodos. Sacar ese despacho fuera del bucle también puede chocar con principios de diseño. Por ejemplo, al dibujar una imagen en un canvas, en vez de llamar repetidamente aputpixel, usar un método comoblitEnterCriticalSectionsí deben hacer validaciones fuertes al entrar, incluyendo condicionales. Pero en código de aplicación, mover elifal llamador puede estar bien. En librerías o código central, es apropiado mover el flujo de control hacia los bordes. Dentro del dominio que controlas, conviene poner el flujo de control en los bordes. Pero como siempre, estas reglas son solo pautas; alguien capaz de juzgar con criterio según el contexto tiene que decidir caso por casomatchcon una llamada a método polimórfica. La idea es separar el momento en que decides la rama condicional inicial del momento en que se ejecuta el comportamiento real. La distinción entre casos la lleva el objeto (aquí, el valor del enum) o un closure, así que no hace falta repetirla en cada invocación. Si cambia la distinción entre casos, solo hay que cambiar el punto de bifurcación, y no el lugar donde ocurre la acción real. La desventaja es el trade-off entre la comodidad de poder ver directamente la rama de comportamiento de cada caso y el hecho de introducir una dependencia a nivel de código respecto de la lista de casosabstraction on atomic apply), y eso no es algo que quepa de forma sencilla en una sola funciónifhacia abajo. Pero este artículo recomienda lo contrario: subir losif, o sea, moverlos a funciones de nivel más alto. Así puedes manejar de forma centralizada la lógica de bifurcación compleja en una sola función y delegar el trabajo concreto real a subrutinasif (weShouldDoThis()) { doThis(); }. Si sacas cada check a una función aparte, es más fácil probarlo y gestionar la complejidadsonarqubey similares reportan a lo loco hasta “code smell” que no son bugs reales. Intentar corregir incluso el “código que no es problemático” de esta manera aumenta mucho el riesgo de introducir bugs nuevos y solo hace perder tiempo que debería dedicarse a problemas realmente importantesifdentro de unfores más fácil de mantener y más eficiente en acceso a memoria que uniffuera delfor. Un ejemplo concreto: si quieres hacer una operación donde a los impares se les suma 1 y a los pares se les resta 2, normalmente tendrías que tomar una rama en cada iteración del bucle, pero con SIMD puedes procesar 16intal mismo tiempo, sin ramas. Si el compilador vectoriza bien, puede convertir el código original en una versión optimizada sin ramasbeforeque presentaste no encaja tanto con el punto central del artículo, y que de hecho la versión SIMD optimizada sí coincide con la idea principal del texto. En el ejemplo, elifdentro delfordepende de los datos, así que no se puede subir fácilmente. Si el algoritmo tuviera una estructura comoif (length % 2 == 1) { ... } else { ... }, donde la condición está fuera del bucle, entonces obviamente sí conviene sacar esa condición por encima delfor. En la versión SIMD elifdesaparece por completo, y ese es el tipo de patrón de código ideal que probablemente también le gustaría al autor del artículofor. ¿Alguien sabe qué tan difícil es para un compilador autovectorizar ese tipo de código? Me da curiosidad dónde está el límiteforcrea una abstracción donde “elfores la regla y la bifurcación es la acción”. Pero cuando aparece un requisito nuevo, esa abstracción se rompe, y empiezas a meter parámetros a la fuerza o a multiplicar los casos especiales, haciendo que el código sea más difícil de entender. Si desde el principio hubieras escrito el código sin esa abstracción, el resultado probablemente habría sido más claro y más fácil de mantener. https://sandimetz.com/blog/2016/1/20/the-wrong-abstraction