¿Por qué tantos desarrolladores creen que el rendimiento, la legibilidad y la mantenibilidad no pueden coexistir?


34

Mientras respondía a esta pregunta , comencé a preguntarme por qué tantos desarrolladores creen que un buen diseño no debería tener en cuenta el rendimiento porque afectaría la legibilidad y / o la capacidad de mantenimiento.

Creo que un buen diseño también tiene en cuenta el rendimiento en el momento en que está escrito, y que un buen desarrollador con un buen diseño puede escribir un programa eficiente sin afectar negativamente la legibilidad o facilidad de mantenimiento.

Si bien reconozco que hay casos extremos, ¿por qué muchos desarrolladores insisten en que un programa / diseño eficiente dará como resultado una legibilidad deficiente y / o una capacidad de mantenimiento deficiente y, en consecuencia, el rendimiento no debería ser una consideración de diseño?


99
Sería casi imposible razonar al respecto a gran escala, pero para pequeños fragmentos de código es bastante obvio. Simplemente compare las versiones legibles y eficientes de, digamos, quicksort.
SK-logic

77
Mu. Debe comenzar apoyando su afirmación de que muchos desarrolladores insisten en que la eficiencia conduce a la falta de mantenimiento.
Peter Taylor

2
SK-logic: En mi opinión, esa es una de las mejores partes de todos los sitios de intercambio de stack, ya que uno cuestiona lo obvio, que puede ser saludable de vez en cuando. Lo que podría ser obvio para usted podría no serlo para otra persona, y viceversa. :) Compartir es demostrar interés.
Andreas Johansson

2
@Justin, no. Ese hilo me parece presuponer una situación en la que hay una elección forzada entre código eficiente o código mantenible. El interlocutor no dice con qué frecuencia se encuentra en esa situación, y los que responden no parecen afirmar que están en esa situación con frecuencia.
Peter Taylor

2
-1 para la pregunta. Cuando lo leí, pensé que era un hombre de paja para desalojar la única respuesta verdadera: "Porque no usan python".
Ingo

Respuestas:


38

Creo que tales puntos de vista suelen ser reacciones a los intentos de optimización (micro) prematura , que todavía es frecuente, y generalmente hace mucho más daño que bien. Cuando uno intenta contrarrestar tales puntos de vista, es fácil caer, o al menos parecer, el otro extremo.

Sin embargo, es cierto que con el enorme desarrollo de los recursos de hardware en las últimas décadas, para la mayoría de los programas escritos hoy, el rendimiento dejó de ser un factor limitante importante. Por supuesto, uno debe tener en cuenta el rendimiento esperado y alcanzable durante la fase de diseño, a fin de identificar los casos en los que el rendimiento puede ser (venir) un problema importante . Y luego es realmente importante diseñar para el rendimiento desde el principio. Sin embargo, la simplicidad general, la legibilidad y la facilidad de mantenimiento son aún más importantes . Como otros señalaron, el código de rendimiento optimizado es más complejo, más difícil de leer y mantener, y más propenso a errores que la solución de trabajo más simple. Por lo tanto, cualquier esfuerzo dedicado a la optimización debe ser probado , no solo creído- traer beneficios reales, mientras se degrada la mantenibilidad a largo plazo del programa lo menos posible. Por lo tanto, un buen diseño aísla las partes complejas e intensivas en rendimiento del resto del código , que se mantiene lo más simple y limpio posible.


8
"Cuando uno intenta contrarrestar tales puntos de vista, es fácil caer, o al menos parecer, el otro extremo". Tengo problemas todo el tiempo con las personas que piensan que tengo el punto de vista opuesto cuando simplemente estoy equilibrando a los profesionales con los contras. No solo en programación, en todo.
jhocking

1
Estoy tan harto de todo el mundo discutiendo sobre esto que me enfado y tomo extremos ..
Thomas Bonini

Ha habido varias buenas respuestas, pero creo que la suya hizo el mejor intento de detallar los orígenes de esta mentalidad. Gracias a todos los involucrados!
justin

Mi respuesta ... la mayoría de los desarrolladores son malos en sus trabajos
TheCatWhisperer

38

En respuesta a su pregunta del lado de un desarrollador que trabaja en código de alto rendimiento, hay varias cosas a tener en cuenta en el diseño.

  • No lo peses prematuramente. Cuando tenga la opción de elegir entre dos diseños de igual complejidad, elija el que tenga las mejores características de rendimiento. Uno de los famosos ejemplos de C ++ es la prevalencia de post-incremento de contadores (o iteradores) en bucles. Esta es una pesimización prematura totalmente innecesaria que PUEDE no costarle nada, pero PUEDE, así que no lo haga.
  • En muchos casos, todavía no tiene negocio para acercarse a la microoptimización. Las optimizaciones algorítmicas son una fruta de bajo rendimiento y casi siempre son mucho más fáciles de entender que las optimizaciones de nivel realmente bajo.
  • Si y SOLO si el rendimiento es absolutamente crítico, te pones sucio. En realidad, aísla el código tanto como puede primero, y LUEGO se ensucia. Y se ensucia mucho allí, con esquemas de almacenamiento en caché, evaluación perezosa, optimización del diseño de memoria para el almacenamiento en caché, bloques de intrínsecos o ensamblaje en línea, capa por capa de plantillas, etc. Usted prueba y documenta como loco aquí, sabe que va lastimar si tiene que hacer algún mantenimiento en este código, pero debe hacerlo porque el rendimiento es absolutamente crítico. Editar: Por cierto, no digo que este código no pueda ser hermoso, y debe hacerse tan hermoso como sea posible, pero seguirá siendo muy complejo y a menudo complicado en comparación con un código menos optimizado.

Hazlo bien, hazlo hermoso, hazlo rápido. En ese orden.


Me gusta la regla general: 'hazlo hermoso, hazlo rápido. En ese orden'. Voy a comenzar a usar eso.
Martin York

Exactamente. Y aísle el código en el tercer punto, tanto como sea posible. Porque cuando pasa a hardware diferente, incluso algo tan pequeño como un procesador con un tamaño de caché diferente, estas cosas pueden cambiar.
KeithB

@KeithB - ​​haces un buen punto, lo agregaré a mi respuesta.
Joris Timmermans

+1: "Hazlo bien, hazlo hermoso, hazlo rápido. En ese orden". Muy buen resumen, con el que estoy de acuerdo el 90%. A veces solo puedo solucionar ciertos errores (hacerlo bien) una vez que lo veo hermoso (y más comprensible).
Giorgio

+1 para "No pesimizar prematuramente". El consejo para evitar la optimización prematura no es el permiso para emplear algoritmos descabellados. Si está escribiendo Java, y tiene una colección a la que recurrirá containsmucho, use un HashSet, no un ArrayList. El rendimiento puede no importar, pero no hay razón para no hacerlo. Explote las congruencias entre un buen diseño y rendimiento: si procesa alguna colección, intente hacer todo en una sola pasada, lo que probablemente sea más legible y más rápido (probablemente).
Tom Anderson

16

Si puedo presumir de "tomar prestado" el bonito diagrama de @ greengit, y hacer una pequeña adición:

|
P
E
R
F
O  *               X <- a program as first written
R   * 
M    *
A      *
N        *
C          *  *   *  *  *
E
|
O -- R E A D A B I L I T Y --

A todos nos han "enseñado" que hay curvas de compensación. Además, todos hemos asumido que somos programadores tan óptimos que cualquier programa que escribimos es tan ajustado que está en la curva . Si un programa está en la curva, cualquier mejora en una dimensión necesariamente incurre en un costo en la otra dimensión.

En mi experiencia, los programas solo se acercan a cualquier curva al ser ajustados, ajustados, martillados, encerados y, en general, convertidos en "código de golf". La mayoría de los programas tienen mucho margen de mejora en todas las dimensiones. Esto es lo que quiero decir.


Personalmente, creo que hay otro extremo de la curva donde sube de nuevo en el lado derecho (siempre que te muevas lo suficiente hacia la derecha (lo que probablemente significa repensar tu algoritmo)).
Martin York

2
+1 para "La mayoría de los programas tienen mucho margen de mejora en todas las dimensiones".
Steven

5

Precisamente porque los componentes de software de alto rendimiento son generalmente órdenes de magnitud más complejos que otros componentes de software (todas las demás cosas son iguales).

Incluso entonces, no es tan claro, si las métricas de rendimiento son un requisito de importancia crítica, entonces es imperativo que el diseño tenga complejidad para cumplir dichos requisitos. El peligro es un desarrollador que desperdicia un sprint en una característica relativamente simple tratando de exprimir algunos milisegundos adicionales de su componente.

Independientemente, la complejidad del diseño tiene una correlación directa con la capacidad de un desarrollador para aprender rápidamente y familiarizarse con dicho diseño, y las modificaciones adicionales a la funcionalidad en un componente complejo pueden dar lugar a errores que las pruebas unitarias no pueden detectar. Los diseños complejos tienen muchas más facetas y posibles casos de prueba para considerar hacer que el objetivo de una cobertura de prueba de unidad del 100% sea aún más un sueño imposible.

Dicho esto, debe tenerse en cuenta que un componente de software con un rendimiento deficiente podría funcionar mal solo porque fue tontamente escrito e innecesariamente complejo basado en la ignorancia del autor original, (haciendo 8 llamadas a la base de datos para construir una sola entidad cuando solo uno lo haría , código completamente innecesario que resulta en una sola ruta de código independientemente, etc. ...) Estos casos son más una cuestión de mejorar la calidad del código y el aumento del rendimiento como consecuencia del refactorizador y NO necesariamente la consecuencia prevista.

Sin embargo, suponiendo un componente bien diseñado, siempre será menos complejo que un componente igualmente bien diseñado y ajustado para el rendimiento (todas las demás cosas son iguales).


3

No es tanto que esas cosas no puedan coexistir. El problema es que el código de todos es lento, ilegible e imposible de mantener en la primera iteración. El resto del tiempo se dedica a mejorar lo que sea más importante. Si eso es rendimiento, entonces adelante. No escriba código atrozmente horrible, pero si solo tiene que ser X rápido, entonces hágalo X rápido. Creo que el rendimiento y la limpieza están básicamente sin correlación. El código de rendimiento no causa código feo. Sin embargo, si pasas tu tiempo ajustando cada bit de código para que sea rápido, ¿adivinas qué no hiciste? Haciendo su código limpio y mantenible.


2
    El |
    PAGS
    mi
    R
    F
    O *
    R * 
    M *
    UNA *
    N *
    C * * * * *
    mi
    El |
    O - READABILIDAD -

Como puedes ver...

  • Sacrificar la legibilidad puede aumentar el rendimiento, pero solo tanto. Después de cierto punto, debe recurrir a medios "reales" como mejores algoritmos y hardware.
  • Además, la pérdida de rendimiento a costa de la legibilidad solo puede suceder en cierta medida. Después de eso, puede hacer que su programa sea lo más legible que desee sin afectar el rendimiento. Por ejemplo, agregar más comentarios útiles no afecta el rendimiento.

Por lo tanto, el rendimiento y la legibilidad están modestamente relacionados, y en la mayoría de los casos, no hay grandes incentivos reales que prefieran lo primero a lo último. Y estoy hablando aquí de idiomas de alto nivel.


1

En mi opinión, el rendimiento debería ser una consideración cuando se trata de un problema real (o, por ejemplo, un requisito). No hacerlo tiende a conducir a microoptimizaciones, lo que podría conducir a un código más ofuscado solo para ahorrar unos microsegundos aquí y allá, lo que a su vez conduce a un código menos fácil de mantener y menos legible. En su lugar, uno debe centrarse en los cuellos de botella reales del sistema, si es necesario , y poner énfasis en el rendimiento allí.


1

El punto no es la legibilidad siempre debe superar la eficiencia. Si sabe desde el principio que su algoritmo necesita ser altamente eficiente, entonces será uno de los factores que utilizará para desarrollarlo.

El caso es que la mayoría de los casos de uso no necesitan un código rápido cegador. En muchos casos, la interacción IO o de usuario causa mucho más retraso que la causa de la ejecución de su algoritmo. El punto es que no debes esforzarte para hacer algo más eficiente si no sabes que es el cuello de la botella.

La optimización del código para el rendimiento a menudo lo hace más complicado porque generalmente implica hacer las cosas de una manera inteligente, en lugar de la más intuitiva. El código más complicado es más difícil de mantener y más difícil para otros desarrolladores (ambos son costos que deben considerarse). Al mismo tiempo, los compiladores son muy buenos para optimizar casos comunes. Es posible que su intento de mejorar un caso común signifique que el compilador ya no reconoce el patrón y, por lo tanto, no puede ayudarlo a acelerar su código. Cabe señalar que esto no significa escribir lo que quiera sin preocuparse por el rendimiento. No debe hacer nada que sea claramente ineficiente.

El punto es no preocuparse por pequeñas cosas que podrían mejorar las cosas. Use un generador de perfiles y vea que 1) lo que tiene ahora es un problema y 2) lo que cambió fue una mejora.


1

Creo que la mayoría de los programadores tienen ese presentimiento simplemente porque la mayoría de las veces, el código de rendimiento se basa en mucha más información (sobre el contexto, el conocimiento del hardware, la arquitectura global) que cualquier otro código en las aplicaciones. La mayoría del código solo expresará algunas soluciones a problemas específicos que se encapsulan en algunas abstracciones de forma modular (como funciones) y eso significa limitar el conocimiento del contexto solo a lo que ingresa a esa encapsulación (como parámetros de función).

Cuando escribe para obtener un alto rendimiento, después de corregir cualquier opción algorítmica, entra en detalles que requieren mucho más conocimiento sobre el contexto. Naturalmente, eso podría abrumar a cualquier programador que no se sienta lo suficientemente concentrado para la tarea.


1

Debido a que el costo del calentamiento global (de esos ciclos de CPU adicionales escalados por cientos de millones de PC más las instalaciones de centros de datos masivos) y la duración mediocre de la batería (en los dispositivos móviles del usuario), como se requiere para ejecutar su código mal optimizado, rara vez aparece en la mayoría Rendimiento del programador o revisiones por pares.

Es una externalidad económica negativa, similar a una forma de contaminación ignorada. Entonces, la relación costo / beneficio de pensar en el rendimiento está mentalmente sesgada de la realidad.

Los diseñadores de hardware han estado trabajando duro para agregar funciones de ahorro de energía y escala de reloj a las últimas CPU. Depende de los programadores dejar que el hardware aproveche estas capacidades con mayor frecuencia, al no masticar cada ciclo de reloj de CPU disponible.

AGREGADO: En la antigüedad, el costo de una computadora era de millones, por lo que era muy importante optimizar el tiempo de CPU. Luego, el costo de desarrollar y mantener el código se hizo mayor que el costo de las computadoras, por lo que la optimización cayó en desgracia en comparación con la productividad del programador. Ahora, sin embargo, otro costo es cada vez mayor que el costo de las computadoras, el costo de alimentar y enfriar todos esos centros de datos ahora es cada vez mayor que el costo de todos los procesadores internos.


Además de la pregunta de si las PC contribuyeron al calentamiento global, incluso si fuera real: es una falacia, que una mayor eficiencia energética conduce a una menor demanda de energía. Casi lo contrario es cierto, como se puede ver desde el primer día que apareció una PC en el mercado. Antes de eso, algunos cientos o miles de Mainframe (cada uno equipado virtualmente con su propia planta de energía) usaban mucha menos energía que hoy, donde 1 minuto de CPU calcula mucho más que entonces a una fracción del costo y la demanda de energía. Sin embargo, la demanda total de energía para la informática es más alta que antes.
Ingo

1

Creo que es difícil lograr los tres. Dos, creo que pueden ser factibles. Por ejemplo, creo que es posible lograr eficiencia y legibilidad en algunos casos, pero la mantenibilidad puede ser difícil con el código microajustado. El código más eficiente en el planeta generalmente carecen tanto de mantenimiento y facilidad de lectura que es probablemente obvio para la mayoría, a menos que usted es el tipo que puede comprender la parte vectorizada en SOA, multihilo código SIMD que Intel escribe con el montaje inline, o la más avanzada algoritmos de bordes utilizados en la industria con documentos matemáticos de 40 páginas publicados hace solo 2 meses y 12 bibliotecas de código para una estructura de datos increíblemente compleja.

Microeficiencia

Una cosa que sugeriría que podría ser contraria a la opinión popular es que el código algorítmico más inteligente es a menudo más difícil de mantener que el algoritmo directo más microajustado. Esta idea de que las mejoras de escalabilidad producen más por el dinero sobre el código microajustado (por ejemplo: patrones de acceso amigables para caché, subprocesos múltiples, SIMD, etc.) es algo que desafiaría, al menos haber trabajado en una industria llena de extremadamente complejo estructuras de datos y algoritmos (la industria visual FX), especialmente en áreas como el procesamiento de mallas, porque la explosión puede ser grande pero el dinero es extremadamente costoso cuando se introducen nuevos algoritmos y estructuras de datos de los que nadie ha oído hablar antes desde que son marcas nuevo. Además, yo '

Entonces, esta idea de que las optimizaciones algorítmicas siempre triunfan, por ejemplo, las optimizaciones relacionadas con los patrones de acceso a la memoria, siempre es algo con lo que no estoy de acuerdo. Por supuesto, si está utilizando un tipo de burbuja, ninguna cantidad de micro-optimización puede ayudarlo allí ... pero dentro de lo razonable, no creo que siempre sea tan claro. Y podría decirse que las optimizaciones algorítmicas son más difíciles de mantener que las microoptimizaciones. Me resultaría mucho más fácil mantener, por ejemplo, el Embree de Intel, que toma un algoritmo BVH clásico y directo y simplemente le saca el micro que el código OpenVDB de Dreamwork para formas innovadoras de acelerar la simulación de fluidos algorítmicamente. Entonces, al menos en mi industria, me gustaría ver a más personas familiarizadas con la arquitectura de la computadora micro-optimizando más, como lo hizo Intel cuando entraron en la escena, en lugar de crear miles y miles de nuevos algoritmos y estructuras de datos. Con micro optimizaciones efectivas, las personas podrían encontrar cada vez menos razones para inventar nuevos algoritmos.

Trabajé en una base de código heredada antes donde casi todas las operaciones de usuario tenían su propia estructura de datos y algoritmo únicos (sumando cientos de estructuras de datos exóticas). Y la mayoría de ellos tenían características de rendimiento muy sesgadas, siendo muy estrictamente aplicables. Hubiera sido mucho más fácil si el sistema pudiera girar en torno a un par de docenas de estructuras de datos más ampliamente aplicables, y creo que podría haber sido el caso si se hubieran micro optimizado mucho mejor. Menciono este caso porque la microoptimización puede mejorar enormemente la capacidad de mantenimiento en tal caso si significa la diferencia entre cientos de estructuras de datos micropesimizados que ni siquiera pueden usarse de manera segura para fines estrictos de solo lectura que implican fallas de caché restantes y derecho vs.

Lenguajes Funcionales

Mientras tanto, algunos de los códigos más fáciles de mantener que he encontrado fueron razonablemente eficientes pero extremadamente difíciles de leer, ya que fueron escritos en lenguajes funcionales. En general, la legibilidad y la mantenibilidad súper son ideas conflictivas en mi opinión.

Es realmente difícil hacer que el código sea legible, mantenible y eficiente a la vez. Por lo general, debe comprometerse un poco en uno de esos tres, si no dos, como comprometer la legibilidad para la mantenibilidad o comprometer la mantenibilidad para la eficiencia. Por lo general, la mantenibilidad es la que sufre cuando busca muchos de los otros dos.

Legibilidad versus mantenibilidad

Ahora como se dijo, creo que la legibilidad y la mantenibilidad no son conceptos armoniosos. Después de todo, el código más legible para la mayoría de nosotros los mortales se mapea muy intuitivamente a los patrones de pensamiento humano, y los patrones de pensamiento humano son inherentemente propensos a errores: " Si esto sucede, haga esto. Si eso sucede, haga eso. De lo contrario, haga esto. Oops. , Olvidé algo! Si estos sistemas interactúan entre sí, esto debería suceder para que este sistema pueda hacer esto ... oh, espera, ¿qué pasa con ese sistema cuando se dispara este evento?"Olvidé la cita exacta, pero alguien dijo una vez que si Roma se construía como un software, solo tomaría un pájaro aterrizar en una pared para derribarlo. Tal es el caso con la mayoría del software. Es más frágil de lo que a menudo nos importa. Piense. Algunas líneas de código aparentemente inocuo aquí y allá podrían detenerlo hasta el punto de hacernos reconsiderar todo el diseño, y los lenguajes de alto nivel que pretenden ser lo más legibles posible no son excepciones a tales errores de diseño humano. .

Los lenguajes funcionales puros son casi tan invulnerables a esto como uno puede llegar a ser factible (ni siquiera cerca de invulnerable, pero relativamente mucho más cerca que la mayoría). Y eso se debe en parte a que no se asignan intuitivamente al pensamiento humano. No son legibles. Nos imponen patrones de pensamiento que nos obligan a resolver problemas con el menor número posible de casos especiales utilizando la mínima cantidad de conocimiento posible y sin causar efectos secundarios. Son extremadamente ortogonales, permiten que el código a menudo se cambie y se cambie sin sorpresas tan épicas que tenemos que repensar el diseño en un tablero de dibujo, incluso hasta el punto de cambiar de opinión sobre el diseño general, sin tener que volver a escribir todo. No parece que sea más fácil de mantener que eso ... pero el código sigue siendo muy difícil de leer,


1
"Microeficiencia" es como decir "No existe el acceso a la memoria O (1)"
Caleth

0

Un problema es que el tiempo finito del desarrollador significa que lo que sea que intente optimizar le quita el tiempo que dedica a otras cuestiones.

Hay un experimento bastante bueno hecho sobre esto al que se hace referencia en Meyer's Code Complete. Se pidió a diferentes grupos de desarrolladores que optimizaran la velocidad, el uso de la memoria, la legibilidad, la solidez, etc. Se descubrió que sus proyectos obtuvieron puntajes altos en lo que se les pidió que optimizaran, pero más bajos en todas las demás cualidades.


Obviamente se puede dedicar más tiempo pero con el tiempo comience a preguntarse por qué los desarrolladores podrían tomar tiempo libre programación emacs para expresar el amor por sus hijos, y en ese punto que está básicamente Sheldon desde el Big Bang Theory
deworde

0

Porque los programadores experimentados han aprendido que es verdad.

Hemos trabajado con código que es delgado y malo y no tiene problemas de rendimiento.

Hemos trabajado en un montón de código que, para abordar los problemas de rendimiento, es MUY complejo.

Un ejemplo inmediato que me viene a la mente es que mi último proyecto incluyó 8.192 tablas SQL fragmentadas manualmente. Esto era necesario debido a problemas de rendimiento. La configuración para seleccionar de 1 tabla es mucho más simple que seleccionar y mantener 8.192 fragmentos.


0

También hay algunas piezas famosas de código altamente optimizado que doblarán los cerebros de la mayoría de las personas que respaldan el caso de que el código altamente optimizado es difícil de leer y comprender.

Aquí está el más famoso, creo. Tomado de Quake III Arena y atribuido a John Carmak, aunque creo que ha habido varias iteraciones de esta función y no fue creada originalmente por él ( ¿no es genial Wikipedia? ).

float Q_rsqrt( float number )
{
    long i;
    float x2, y;
    const float threehalfs = 1.5F;

    x2 = number * 0.5F;
    y  = number;
    i  = * ( long * ) &y;                       // evil floating point bit level hacking
    i  = 0x5f3759df - ( i >> 1 );               // what the fuck?
    y  = * ( float * ) &i;
    y  = y * ( threehalfs - ( x2 * y * y ) );   // 1st iteration
    //      y  = y * ( threehalfs - ( x2 * y * y ) );   // 2nd iteration, this can be removed

    return y;
}
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.