¿El cambio gradual en la metodología de escribir código afectó el rendimiento del sistema? ¿Y debería importarme?

35

TD; DR:

Hubo cierta confusión en cuanto a lo que estaba preguntando, así que aquí está la idea principal detrás de la pregunta:

Siempre quise que la pregunta fuera qué es. Puede que no lo haya articulado bien originalmente. Pero la intención siempre ha sido " es un código modular, separado, desacoplado, desacoplado y refactorizado " notablemente más lento por su propia naturaleza que el código " monolítico de una sola unidad, hacer todo en un solo lugar, un archivo, estrechamente acoplado ". El resto son solo detalles y varias manifestaciones de esto que encontré entonces o ahora o lo haré más tarde. Es más lento con seguridad en alguna escala. Al igual que un disco no desfragmentado, debe recoger las piezas de todas partes. Es mas lento. Sin lugar a duda. ¿Pero debería importarme?

Y la pregunta no es sobre ...

no se trata de micro-optimización, optimización prematura, etc. No se trata de "optimizar esta o aquella parte hasta la muerte".

¿Entonces que es?

Se trata de la metodología general y las técnicas y formas de pensar sobre la escritura de código que surgió con el tiempo:

  • "inyecta este código en tu clase como una dependencia"
  • "escribe un archivo por clase"
  • "separe su vista de su base de datos, controlador, dominio".
  • no escriba spaghetti homogeneious single codeblock, sino que escriba muchos componentes modulares separados que trabajen juntos

Se trata de la forma y el estilo del código que actualmente, dentro de esta década, es visto y defendido en la mayoría de los marcos, defendido en las convenciones, transmitido a través de la comunidad. Es un cambio en el pensamiento de 'bloques monolíticos' a 'microservicios'. Y con eso viene el precio en términos de rendimiento y gastos generales a nivel de máquina, y también algunos gastos generales a nivel de programador.

La pregunta original sigue:

En el campo de la informática, he notado un cambio notable en el pensamiento cuando se trata de programación. A menudo me encuentro con el consejo que dice así:

  • escribir código más pequeño en función de las funciones (más comprobable y mantenible de esta manera)
  • refactorice el código existente en fragmentos de código cada vez más pequeños hasta que la mayoría de sus métodos / funciones tengan solo unas pocas líneas y quede claro cuál es su propósito (que crea más funciones, en comparación con un bloque monolítico más grande)
  • escribir funciones que solo hacen una cosa: separación de preocupaciones, etc. (que generalmente crea más funciones y más marcos en una pila)
  • cree más archivos (una clase por archivo, más clases para propósitos de descomposición, para propósitos de capa como MVC, arquitectura de dominio, patrones de diseño, OO, etc., que crea más llamadas al sistema de archivos)

Este es un cambio en comparación con las prácticas de codificación "antiguas" u "anticuadas" o "espaguetis" en las que tiene métodos que abarcan 2500 líneas, y grandes clases y objetos divinos que hacen todo.

Mi pregunta es esta:

cuando se trata de código de máquina, de 1s y 0s, de instrucciones de ensamblaje, de discos HDD, ¿debería preocuparme si mi código OO perfectamente separado por clases con una variedad de funciones y métodos refactorizados de pequeño a pequeño también genera mucho más gastos generales?

Detalles

Si bien no estoy muy familiarizado con la forma en que el código OO y sus llamadas a métodos se manejan en ASM al final, y cómo las llamadas DB y las llamadas del compilador se traducen en mover el brazo del actuador en un disco HDD, tengo una idea. Supongo que cada llamada de función adicional, llamada de objeto o llamada "#include" (en algunos idiomas) genera un conjunto adicional de instrucciones, lo que aumenta el volumen del código y agrega varios gastos generales de "cableado de código", sin agregar el código "útil" real . También imagino que se pueden hacer buenas optimizaciones para ASM antes de que realmente se ejecute en el hardware, pero esa optimización solo puede hacer mucho.

Por lo tanto, mi pregunta: cuánta sobrecarga (en espacio y velocidad) hace el código bien separado (código que se divide en cientos de archivos, clases y patrones de diseño, etc.) realmente introduce en comparación con tener "un gran método que contiene todo en un archivo monolítico ", debido a esta sobrecarga?

ACTUALIZACIÓN para mayor claridad:

Supongo que tomar el mismo código y dividirlo, refactorizarlo, desacoplarlo en más y más funciones, objetos, métodos y clases dará como resultado que más y más parámetros pasen entre piezas de código más pequeñas. Porque seguro, el código de refactorización debe mantener el hilo en funcionamiento, y eso requiere pasar parámetros. Más métodos o más clases o más patrones de diseño de Métodos de Fábrica dan como resultado una mayor sobrecarga de pasar varios bits de información más de lo que es el caso en una sola clase o método monolítico.

Se dijo en alguna parte (cita TBD) que hasta el 70% de todo el código está compuesto por la instrucción MOV de ASM, que carga los registros de la CPU con las variables adecuadas, no el cálculo real que se realiza. En mi caso, carga el tiempo de la CPU con instrucciones PUSH / POP para proporcionar la vinculación y el paso de parámetros entre varias piezas de código. Cuanto más pequeño sea su código, más se requiere un "enlace" superior. Me preocupa que este vínculo se sume a la hinchazón y la desaceleración del software y me pregunto si debería preocuparme por esto y cuánto, si es que lo hay, porque las generaciones actuales y futuras de programadores que están construyendo software para el próximo siglo , tendrá que vivir y consumir software creado utilizando estas prácticas.

ACTUALIZACIÓN: Múltiples archivos

Estoy escribiendo un código nuevo ahora que está reemplazando lentamente el código viejo. En particular, he notado que una de las clases antiguas era un archivo de ~ 3000 líneas (como se mencionó anteriormente). Ahora se está convirtiendo en un conjunto de 15-20 archivos ubicados en varios directorios, incluidos los archivos de prueba y sin incluir el marco PHP que estoy usando para unir algunas cosas. También vienen más archivos. Cuando se trata de E / S de disco, cargar varios archivos es más lento que cargar un archivo grande. Por supuesto, no todos los archivos se cargan, se cargan según sea necesario, y existen opciones de almacenamiento en caché de disco y memoria caché, y aún así creo que loading multiple filesrequiere más procesamiento que loading a single fileen la memoria. Estoy agregando eso a mi preocupación.

ACTUALIZACIÓN: la dependencia inyecta todo

Volviendo a esto después de un tiempo ... Creo que mi pregunta fue mal entendida. O tal vez elegí entender mal algunas respuestas. No estoy hablando de micro-optimización, ya que algunas respuestas han señalado (al menos creo que llamar a lo que estoy hablando de micro-optimización es un nombre inapropiado) sino sobre el movimiento del "Código de refactorización para aflojar el acoplamiento apretado", en general. , en todos los niveles del código. Vengo de Zend Con hace poco, donde este estilo de código ha sido uno de los puntos centrales y centrales de la convención. Desacoplar la lógica de la vista, la vista del modelo, el modelo de la base de datos y, si puede, desacoplar los datos de la base de datos. Dependencia: inyecte todo, lo que a veces significa simplemente agregar código de cableado (funciones, clases, repetitivo) que no hace nada, pero sirve como punto de costura / enganche, duplicando fácilmente el tamaño del código en la mayoría de los casos.

ACTUALIZACIÓN 2: ¿"separar el código en más archivos" afecta significativamente el rendimiento (en todos los niveles de computación)

¿Cómo afecta la filosofía de compartmentalize your code into multiple filesla informática actual (rendimiento, utilización de disco, gestión de memoria, tareas de procesamiento de CPU)?

estoy hablando de

Antes de...

En un pasado hipotético pero bastante real, no muy lejano, podría escribir fácilmente un monobloque de un archivo que tenga espagueti de modelo y vista y controlador o no codificado con espagueti, pero que ejecute todo una vez que ya esté cargado. Al hacer algunos puntos de referencia en el pasado usando el código C, descubrí que es MUCHO más rápido cargar un solo archivo de 900Mb en la memoria y procesarlo en grandes trozos que cargar un montón de archivos más pequeños y procesarlos en una comida de paz más pequeña trozos haciendo el mismo trabajo al final.

.. Y ahora*

Hoy me encuentro mirando el código que muestra un libro mayor, que tiene características como ... si un artículo es un "pedido", muestra el bloque HTML de pedido. Si se puede copiar una línea de pedido, imprima un bloque HTML que muestre un ícono y parámetros HTML detrás que le permitan realizar la copia. Si el elemento se puede mover hacia arriba o hacia abajo, muestre las flechas HTML apropiadas. Etc. Puedo, a través de Zend Framework crearpartial()llamadas, que esencialmente significa "llamar a una función que toma sus parámetros y los inserta en un archivo HTML separado que también llama". Dependiendo de cuán detallado quiera obtener, puedo crear funciones HTML separadas para las partes más pequeñas del libro mayor. Uno para flecha arriba, flecha abajo, uno para "¿puedo copiar este elemento", etc. Crear fácilmente varios archivos solo para mostrar una pequeña parte de la página web. Tomando mi código y el código Zend Framework detrás de escena, el sistema / pila probablemente llama cerca de 20-30 archivos diferentes.

¿Qué?

Estoy interesado en aspectos, el desgaste de la máquina que se crea compartimentando el código en muchos archivos separados más pequeños.

Por ejemplo, cargar más archivos significa tenerlos ubicados en varios lugares del sistema de archivos y en varios lugares de HDD físico, lo que significa más tiempo de búsqueda y lectura de HDD.

Para la CPU, probablemente significa más cambio de contexto y carga de varios registros.

En este subbloque (actualización n. ° 2), estoy interesado más estrictamente en cómo el uso de múltiples archivos para realizar las mismas tareas que podrían realizarse en un solo archivo, afecta el rendimiento del sistema.

Usando Zend Form API vs HTML simple

Utilicé Zend Form API con las últimas y mejores prácticas modernas de OO, para construir un formulario HTML con validación, transformándolo POSTen objetos de dominio.

Me tomó 35 archivos para hacerlo.

35 files = 
    = 10 fieldsets x {programmatic fieldset + fieldset manager + view template} 
    + a few supporting files

Todo lo cual podría ser reemplazado por unos pocos archivos HTML + PHP + JS + CSS simples, quizás un total de 4 archivos livianos.

¿Es mejor? ¿Vale la pena? ... Imagine cargar 35 archivos + numerosos archivos de la biblioteca Zend Zramework que los hacen funcionar, frente a 4 archivos simples.

Dennis
fuente
1
Impresionante pregunta. Haré algunas evaluaciones comparativas (tómate un día más o menos para encontrar algunos buenos casos). Sin embargo, las mejoras de velocidad en este grado tienen un costo enorme para la legibilidad y los costos de desarrollo. Mi conjetura inicial es que el resultado es un aumento insignificante del rendimiento.
Dan Sabin
77
@Dan: ¿Puedes incluirlo en tu calendario para comparar el código después de 1, 5 y 10 años de mantenimiento? Si recuerdo, volveré a consultar los resultados :)
mattnz
1
Sí, ese es el verdadero pateador. Creo que todos estamos de acuerdo en que hacer menos búsquedas y llamadas a funciones es más rápido. Sin embargo, no puedo imaginar un caso en el que se prefiera a cosas como entrenar de manera fácil y fácil a los nuevos miembros del equipo.
Dan Sabin
2
Para aclarar, ¿tiene requisitos de velocidad específicos para su proyecto? ¿O simplemente quieres que tu código "vaya más rápido"! Si es lo último, no me preocuparía, el código que es lo suficientemente rápido pero fácil de mantener es mucho, mucho mejor que el código que es más rápido que lo suficientemente rápido pero es un desastre.
Cormac Mulhall
44
La idea de evitar llamadas a funciones por razones de rendimiento es exactamente el tipo de pensamiento loco que Dijkstra criticó en su famosa cita sobre la optimización prematura. En serio, no puedo
RibaldEddie

Respuestas:

10

Mi pregunta es la siguiente: cuando la llamada se reduce al código de la máquina, a 1s y 0s, a las instrucciones de ensamblaje, ¿debería preocuparme en absoluto de que mi código separado por clases con una variedad de funciones pequeñas a pequeñas genere demasiada sobrecarga adicional?

MI respuesta es sí, deberías. No porque tengas muchas funciones pequeñas (alguna vez la sobrecarga de las funciones de llamada era razonablemente significativa y podrías ralentizar tu programa haciendo un millón de pequeñas llamadas en bucles, pero hoy los compiladores las incluirán en línea para ti y lo que queda se toma cuidado por los sofisticados algoritmos de predicción de la CPU, así que no te preocupes por eso) sino porque introducirás el concepto de superponer demasiado en tus programas cuando la funcionalidad es demasiado pequeña como para sentirla en tu cabeza. Si tiene componentes más grandes, puede estar razonablemente seguro de que no están realizando el mismo trabajo una y otra vez, pero puede hacer que su programa sea tan minuciosamente detallado que puede no ser capaz de comprender realmente las rutas de llamadas, y en ese caso terminar con algo eso apenas funciona (y apenas se puede mantener).

Por ejemplo, trabajé en un lugar que me mostró un proyecto de referencia para un servicio web con 1 método. El proyecto constaba de 32 archivos .cs, ¡para un único servicio web! Pensé que esto era demasiada complejidad, aunque cada parte era pequeña y fácil de entender por sí misma, cuando se trataba de describir el sistema en general, rápidamente tuve que rastrear las llamadas solo para ver qué demonios estaba haciendo (allí También hubo demasiadas abstracciones involucradas, como era de esperar). Mi servicio web de reemplazo fue 4 archivos .cs.

No medí el rendimiento, ya que supongo que habría sido más o menos el mismo en general, pero puedo garantizar que el mío fue significativamente más barato de mantener. Cuando todo el mundo habla de que el tiempo del programador es más importante que el tiempo de la CPU, crea monstruos complejos que cuestan mucho tiempo del programador tanto en desarrollo como en mantenimiento. Tienes que preguntarte si están inventando excusas para el mal comportamiento.

Se dijo en alguna parte (cita TBD) que hasta el 70% de todo el código está compuesto por la instrucción MOV de ASM, que carga los registros de la CPU con las variables adecuadas, no el cálculo real que se realiza.

Sin embargo, eso es lo que hacen las CPU: mueven bits de la memoria a los registros, los suman o restan, y luego los vuelven a guardar en la memoria. Toda la informática se reduce a más o menos eso. Eso sí, una vez tuve un programa muy multiproceso que pasó la mayor parte de su tiempo cambiando de contexto (es decir, guardando y restaurando el estado del registro de los hilos) que trabajando en el código del hilo. Un simple bloqueo en el lugar equivocado realmente atornillaba el rendimiento allí, y también era un código tan inocuo.

Entonces, mi consejo es: encuentre un punto medio sensible entre ambos extremos que haga que su código se vea bien para otros humanos, y pruebe el sistema para ver si funciona bien. Use las funciones del sistema operativo para asegurarse de que se ejecute como esperaría con CPU, memoria, disco y E / S de red.

gbjbaanb
fuente
Creo que esto me habla más en este momento. Me sugiere que un buen término medio es comenzar con conceptos de mapeo mental para codificar piezas mientras se siguen y usan ciertos conceptos (como DI), en lugar de atarse a la descomposición de código esotérico y volverse loco (es decir, DI todo si lo necesita o no).
Dennis
1
Personalmente, encuentro que un código más "moderno" es algo más fácil de perfilar ... Supongo que cuanto más fácil de mantener es un fragmento de código, también es más fácil de perfilar, pero hay un límite, donde dividir las cosas en pequeños pedazos las hace menos mantenible ...
AK_
25

Sin un cuidado extremo, la micro optimización como estas preocupaciones conduce a un código que no se puede mantener.

Inicialmente parece una buena idea, el generador de perfiles le dice que el código es más rápido y V & V / Test / QA incluso dice que funciona. Pronto se encuentran errores, los requisitos cambian y se solicitan mejoras que nunca se consideraron.

A lo largo de la vida de un proyecto, el código se degrada y se vuelve menos eficiente. El código mantenible se volverá más eficiente que su contraparte no mantenible, ya que se degradará más lentamente. La razón es que el código genera entropía a medida que cambia:

El código que no se puede mantener rápidamente tiene más código muerto, rutas redundantes y duplicación. Esto lleva a más errores, creando un ciclo de degradación del código, incluido su rendimiento. En poco tiempo, los desarrolladores tienen poca confianza en que los cambios que están haciendo sean correctos. Esto los ralentiza, hace que sean cautelosos y generalmente conduce a una entropía aún mayor, ya que abordan solo los detalles que pueden ver

El código que se puede mantener, con módulos pequeños y pruebas unitarias es más fácil de cambiar, el código que ya no se necesita es más fácil de identificar y eliminar. El código que está roto también es más fácil de identificar, puede repararse o reemplazarse con confianza.

Así que al final todo se reduce a la gestión del ciclo de vida y no es tan simple como "esto es más rápido, por lo que siempre será más rápido".

Sobre todo, el código correcto lento es infinitamente más rápido que el código incorrecto rápido.

Mattnz
fuente
Thankx Para conducir esto un poco hacia donde iba, no estoy hablando de micro-optimización, sino de un movimiento global hacia la escritura de código más pequeño, incorporando más inyección de dependencia y, por lo tanto, más piezas de funcionalidad externas y más "partes móviles" de código en general. que todos necesitan estar conectados para que funcionen. Tiendo a pensar que esto produce más enlace / conector / paso variable / MOV / PUSH / POP / CALL / JMP fluff a nivel de hardware. También veo un valor en el cambio hacia la legibilidad del código, aunque a expensas de sacrificar los ciclos informáticos a nivel de hardware para "fluff".
Dennis
10
¡Evitar llamadas a funciones por razones de rendimiento es absolutamente una microoptimización! Seriamente. No puedo pensar en un mejor ejemplo de micro-optimización. ¿Qué evidencia tiene de que la diferencia de rendimiento realmente importa para el tipo de software que está escribiendo? Parece que no tienes ninguno.
RibaldEddie
17

Según tengo entendido, como señala con inline, en formas de código de nivel inferior como C ++ puede hacer la diferencia, pero digo CAN a la ligera.

El sitio web lo resume: no hay una respuesta fácil . Depende del sistema, de lo que esté haciendo su aplicación, del idioma, del compilador y de la optimización.

C ++, por ejemplo, en línea puede aumentar el rendimiento. Muchas veces puede no hacer nada, o posiblemente disminuir el rendimiento, pero personalmente nunca me he encontrado con eso, aunque he oído hablar de historias. Inline no es más que una sugerencia para que el compilador optimice, lo que puede ignorarse.

Lo más probable es que, si está desarrollando programas de nivel superior, los gastos generales no deberían ser una preocupación si hay uno para empezar. Los compiladores son extremadamente inteligentes en estos días y de todos modos deberían manejar estas cosas. Muchos programadores tienen un código para vivir: nunca confíes en el compilador. Si esto se aplica a usted, incluso las pequeñas optimizaciones que considere importantes pueden serlo. Pero, tenga en cuenta que cada idioma difiere en este sentido. Java realiza optimizaciones en línea automáticamente en tiempo de ejecución. En Javascript, en línea para su página web (en lugar de archivos separados) es un impulso y cada milisegundo para una página web puede contar, pero eso es más un problema de E / S.

Pero en los programas de nivel inferior en los que el programador podría estar haciendo mucho trabajo de código de máquina junto con algo como C ++, lo escuchado podría marcar la diferencia. Los juegos son un buen ejemplo de donde la canalización de la CPU es crítica, especialmente en consolas, y algo como en línea puede sumar un poco aquí y allá.

Una buena lectura en línea específicamente: http://www.gotw.ca/gotw/033.htm

user99999991
fuente
Desde la perspectiva de mi pregunta, no estoy tan centrado en la línea per seh, sino en el "cableado codificado" que ocupa el proceso de tiempo de CPU, bus y E / S que une varios fragmentos de código. Me pregunto si hay algún punto en el que haya un 50% o más de código de cableado y un 50% del código real que desea ejecutar. Me imagino que hay mucha pelusa incluso en el código más estricto que uno puede escribir, y parece ser un hecho real. Gran parte del código real que se ejecuta a nivel de bits y bytes es logística: mover valores de un lugar a otro, saltar a un lugar u otro, y solo a veces agregar ...
Dennis
... resta u otra función relacionada con el negocio. Al igual que el desenrollado de bucles puede acelerar algunos bucles debido a una menor sobrecarga asignada para incrementar las variables, escribir funciones más grandes probablemente puede agregar algo de velocidad, siempre que su caso de uso esté configurado para ello. Mi preocupación aquí es más general, ver muchos consejos para escribir piezas de código más pequeñas, aumenta este cableado, a la vez que se beneficia al agregar legibilidad (con suerte) y a expensas de la hinchazón a nivel micro.
Dennis
3
@Dennis: una cosa a tener en cuenta es que en un lenguaje OO, puede haber MUY poca correlación entre lo que escribe el programador (a + b) y qué código se genera (¿una simple adición de dos registros? ¿Se mueve primero de la memoria? entonces la función llama al operador de un objeto +?). Por lo tanto, las "funciones pequeñas" a nivel del programador pueden ser cualquier cosa menos pequeñas una vez representadas en el código de la máquina.
Michael Kohne
2
@ Dennis Puedo decir que escribir código ASM (directamente, no compilado) para Windows va en la línea de "mov, mov, invoke, mov, mov, invoke". Con invocar como una macro que hace una llamada envuelta por push / pops ... Ocasionalmente, hará llamadas de función en su propio código, pero todas las llamadas del sistema operativo lo empequeñecen.
Brian Knoblauch
12

Este es un cambio en comparación con las prácticas de código "antiguas" o "malas" en las que tiene métodos que abarcan 2500 líneas y grandes clases que lo hacen todo.

No creo que nadie haya pensado que hacerlo sea una buena práctica. Y dudo que las personas que lo hicieron lo hicieran por razones de rendimiento.

Creo que la famosa cita de Donald Knuth es muy relevante aquí:

Deberíamos olvidarnos de las pequeñas eficiencias, digamos alrededor del 97% del tiempo: la optimización prematura es la raíz de todo mal.

Entonces, en el 97% de su código, solo use buenas prácticas, escriba métodos pequeños (qué tan pequeño es una cuestión de opinión, no creo que todos los métodos deban ser solo unas pocas líneas), etc. Para el 3% restante, donde el rendimiento importa, mídelo . Y si las mediciones muestran que tener muchos métodos pequeños en realidad ralentiza significativamente su código, entonces debe combinarlos en métodos más grandes. Pero no escriba código que no se pueda mantener solo porque podría ser más rápido.

svick
fuente
11

Debe tener cuidado al escuchar a programadores experimentados, así como al pensamiento actual. Las personas que han tratado durante años con software masivo tienen algo que aportar.

En mi experiencia, esto es lo que conduce a la desaceleración, y no son pequeños. Son órdenes de magnitud:

  • La suposición de que cualquier línea de código requiere aproximadamente el mismo tiempo que cualquier otra. Por ejemplo cout << endlversus a = b + c. La primera toma miles de veces más que la segunda. Stackexchange tiene muchas preguntas de la forma "He intentado diferentes formas de optimizar este código, pero no parece hacer la diferencia, ¿por qué no?" cuando hay una llamada de función grande y antigua en el medio.

  • La suposición de que cualquier función o llamada de método, una vez escrita, es por supuesto necesaria. Las funciones y los métodos son fáciles de llamar, y la llamada suele ser bastante eficiente. El problema es que son como las tarjetas de crédito. Tentan a gastar más de lo que realmente quieres, y tienden a ocultar lo que gastaste. Además de eso, el software grande tiene capas sobre capas de abstracción, por lo que incluso si solo hay un 15% de desperdicio en cada capa, más de 5 capas que se combinan con un factor de desaceleración de 2. La respuesta a esto no es eliminar la funcionalidad o escribir funciones más grandes, es disciplinarse para estar en guardia para este problema y estar dispuesto y ser capaz de solucionarlo .

  • Generalidad galopante. El valor de la abstracción es que puede permitirte hacer más con menos código, al menos esa es la esperanza. Esta idea puede ser llevada al extremo. El problema con demasiada generalidad es que cada problema es específico, y cuando lo resuelve con abstracciones generales, esas abstracciones no necesariamente pueden explotar las propiedades específicas de su problema. ¡Por ejemplo, he visto una situación en la que se usaba una clase de cola de prioridad elegante, que podría ser eficiente en tamaños grandes, cuando la longitud nunca excedía de 3!

  • Estructura de datos al galope. OOP es un paradigma muy útil, pero no lo alienta a minimizar la estructura de datos, sino que lo alienta a tratar de ocultar su complejidad. Por ejemplo, existe el concepto de "notificación" en el que si el dato A se modifica de alguna manera, A emite un evento de notificación para que B y C también puedan modificarse para mantener la coherencia de todo el conjunto. Esto puede propagarse en muchas capas y aumentar enormemente el costo de la modificación. Entonces es completamente posible que el cambio a A pueda deshacerse poco despuéso cambiado a otra modificación más, lo que significa que el esfuerzo dedicado a tratar de mantener el conjunto coherente debe hacerse nuevamente. Lo desconcertante es la probabilidad de errores en todos estos manejadores de notificaciones, circularidad, etc. Es mucho mejor tratar de mantener la estructura de datos normalizada, de modo que cualquier cambio deba realizarse en un solo lugar. Si no se pueden evitar los datos no normalizados, es mejor tener pases periódicos para reparar la inconsistencia, en lugar de pretender que pueden mantenerse consistentes con una correa corta.

... cuando pienso en más, lo agregaré.

Mike Dunlavey
fuente
8

La respuesta corta es sí". Y, en general, el código será un poco más lento.

Pero a veces una refactorización OO-ish adecuada revelará optimizaciones que hacen que el código sea más rápido. Trabajé en un proyecto donde creamos un algoritmo Java complejo mucho más OO-ish, con estructuras de datos adecuadas, captadores, etc. en lugar de arreglos de objetos anidados desordenados. Pero, al aislar y restringir mejor el acceso a las estructuras de datos, pudimos cambiar de matrices gigantes de Dobles (con nulos para resultados vacíos) a matrices de dobles más organizadas, con NaN para resultados vacíos. Esto produjo una ganancia de velocidad de 10x.

Anexo: en general, un código más pequeño y mejor estructurado debería ser más apto para subprocesos múltiples, su mejor manera de obtener mayores aceleraciones.

user949300
fuente
1
No veo por qué cambiar de Doubles a doubles requeriría un código mejor estructurado.
svick
Wow, un voto negativo? El código original del cliente no se ocupó de Double.NaNs, pero estaba buscando nulos para representar valores vacíos. Después de la reestructuración, podríamos manejar esto (mediante encapsulación) con los captadores de los diferentes resultados del algoritmo. Claro, podríamos haber reescrito el código del cliente, pero esto fue más fácil.
user949300
Para que conste, no fui yo quien rechazó esta respuesta.
svick
7

Entre otras cosas, la programación se trata de compensaciones . En base a este hecho, me inclino a responder sí, podría ser más lento. Pero piensa en lo que obtienes a cambio. Obtener un código legible, reutilizable y fácilmente modificable supera fácilmente cualquier posible inconveniente.

Como @ user949300 mencionó , es más fácil detectar áreas que pueden mejorarse algorítmica o arquitectónicamente con dicho enfoque; mejorarlos suele ser mucho más beneficioso y efectivo que no tener una posible OO o sobrecarga de llamadas a funciones (lo cual es solo un ruido, apuesto).


También imagino que se pueden hacer buenas optimizaciones para ASM antes de que realmente se ejecute en el hardware.

Siempre que algo así me pasa por la mente, recuerdo que las décadas que pasaron las personas más inteligentes trabajando en compiladores probablemente hacen que herramientas como GCC sean mucho mejores que yo para generar código de máquina. A menos que esté trabajando en algún tipo de material relacionado con microcontroladores, le sugiero que no se preocupe por eso.

Supongo que agregar más y más funciones y más y más objetos y clases en un código dará como resultado que más y más parámetros pasen entre piezas de código más pequeñas.

Suponiendo que algo al optimizar es una pérdida de tiempo, necesita datos sobre el rendimiento del código. Encuentre dónde pasa la mayor parte del tiempo con herramientas especializadas, optimícelo e itere.


Para resumir todo esto; deje que el compilador haga su trabajo, concéntrese en cosas importantes como mejorar sus algoritmos y estructuras de datos. Todos los patrones que mencionaste en tu pregunta existen para ayudarte con eso, úsalos.

PD: Estas 2 conversaciones de Crockford aparecieron en mi cabeza y creo que están algo relacionadas. El primero es la historia súper breve de CS (que siempre es bueno saber con cualquier ciencia exacta); y el segundo es por qué rechazamos las cosas buenas.

revs elmigranto
fuente
Realmente me gusta esta respuesta. Los humanos son horribles al adivinar el compilador y tomar fotos de dónde están los cuellos de botella. por supuesto, la gran complejidad de tiempo de O es algo que debe tener en cuenta, pero el gran método de espagueti de 100 líneas frente a una invocación de método de fábrica + algún envío de método virtual nunca debería ser una discusión. el rendimiento no es nada interesante en ese caso. Además, una gran ventaja para señalar que "optimizar" sin hechos y mediciones es una pérdida de tiempo.
sara
4

Creo que las tendencias que identifica apuntan a una verdad sobre el desarrollo de software: el tiempo del programador es más costoso que el tiempo de la CPU. Hasta ahora, las computadoras solo se han vuelto más rápidas y baratas, pero un desorden enredado de una aplicación puede tomar cientos, si no miles de horas de trabajo para cambiar. Dado el costo de los salarios, los beneficios, el espacio de oficina, etc., etc., es más rentable tener un código que pueda ejecutarse un poco más lento pero que sea más rápido y seguro cambiarlo.

Rory Hunter
fuente
1
Estoy de acuerdo, pero los dispositivos móviles se están volviendo tan populares que creo que son una gran excepción. Aunque la potencia de procesamiento está aumentando, no puede crear una aplicación para iPhone y esperar poder agregar memoria como puede hacerlo en un servidor web.
JeffO
3
@JeffO: No estoy de acuerdo: los dispositivos móviles con procesadores de cuatro núcleos ahora son normales, el rendimiento (especialmente porque afecta la vida útil de la batería), aunque es una preocupación, es menos importante que la estabilidad. Un teléfono móvil o tableta lento recibe malas críticas, uno que es inestable es sacrificado. Las aplicaciones móviles son muy dinámicas, cambian casi a diario: la fuente tiene que mantenerse al día.
mattnz
1
@JeffO: Por lo que vale, la máquina virtual utilizada en Android es bastante mala. Según los puntos de referencia, podría ser un orden de magnitud más lento que el código nativo (mientras que el mejor de la raza suele ser un poco más lento). Adivina qué, a nadie le importa. Escribir la aplicación es rápido y la CPU se queda allí girando sus pulgares y esperando la entrada del usuario el 90% del tiempo de todos modos.
Jan Hudec
Hay más en el rendimiento que los puntos de referencia de CPU sin procesar. Mi teléfono Android funciona bien, excepto cuando el AV está escaneando una actualización y luego parece que se cuelga por más tiempo de lo que me gusta, ¡y es un modelo de 4Gb RAM de cuatro núcleos! Hoy el ancho de banda (ya sea de red o de memoria) es probablemente el principal cuello de botella. Su CPU súper rápida probablemente esté haciendo girar sus pulgares el 99% del tiempo y la experiencia general sigue siendo pobre.
gbjbaanb
4

Bueno, hace más de 20 años, que supongo que no está llamando nuevo y no "viejo o malo", la regla era mantener las funciones lo suficientemente pequeñas como para caber en una página impresa. Teníamos impresoras de matriz de puntos entonces, por lo que el número de líneas era algo fijo, generalmente solo una o dos opciones para el número de líneas por página ... definitivamente menos de 2500 líneas.

Usted está preguntando muchos aspectos del problema, mantenibilidad, rendimiento, capacidad de prueba, legibilidad. Cuanto más se incline hacia el rendimiento, menos se podrá mantener y leer el código, por lo que debe encontrar su nivel de comodidad que puede variar y variará para cada programador individual.

En cuanto al código producido por el compilador (código de máquina, si lo desea), cuanto mayor sea la función, más oportunidades tendrá de derramar valores intermedios en registros a la pila. Cuando se utilizan marcos de pila, el consumo de la pila está en trozos más grandes. Cuanto más pequeñas son las funciones, más oportunidades tienen los datos de permanecer más en los registros y menos dependencia de la pila. Se necesitan trozos de pila más pequeños por función de forma natural. Los marcos de pila tienen ventajas y desventajas para el rendimiento. Más funciones más pequeñas significan más funciones de configuración y limpieza. Por supuesto, también depende de cómo compilas, qué oportunidades le das al compilador. Puede tener 250 funciones de 10 líneas en lugar de una función de 2500 líneas, la función de 2500 líneas que obtendrá el compilador si puede / elige optimizar en todo el proceso. Pero si toma esas 250 funciones de 10 líneas y las distribuye en 2, 3, 4, 250 archivos separados, compila cada archivo por separado, entonces el compilador no podrá optimizar casi tanto código muerto como podría haberlo hecho. La conclusión aquí es que hay ventajas y desventajas para ambos y no es posible establecer una regla general sobre esta o esa es la mejor manera.

Las funciones de tamaño razonable, algo que una persona puede ver en una pantalla o página (en una fuente razonable), es algo que puede consumir mejor y comprender que el código que es bastante grande. Pero si es solo una pequeña función con llamadas a muchas otras funciones pequeñas que llaman a muchas otras funciones pequeñas, necesita varias ventanas o navegadores para comprender ese código, por lo que no compró nada en el lado de la legibilidad.

la forma unix es hacer usando mi término, bloques de lego bien pulidos. ¿Por qué usarías una función de cinta tantos años después de que dejamos de usar cintas? Debido a que el blob hizo su trabajo muy bien y en la parte posterior podemos reemplazar la interfaz de la cinta con una interfaz de archivo y aprovechar la carne del programa. ¿Por qué reescribir el software de grabación de CDROM solo porque SCSI desapareció cuando la interfaz dominante fue reemplazada por IDE (para luego volver)? Nuevamente aproveche los subbloques pulidos y reemplace un extremo con un nuevo bloque de interfaz (también comprenda que los diseñadores de hardware simplemente agregaron un bloque de interfaz en los diseños de hardware para, en algunos casos, hacer que una unidad scsi tenga una idea para la interfaz scsi). , construya bloques de lego pulidos de tamaño razonable, cada uno con un propósito bien definido y entradas y salidas bien definidas. puede ajustar las pruebas alrededor de esos bloques de lego y luego tomar el mismo bloque y envolver la interfaz de usuario y las interfaces del sistema operativo alrededor del mismo bloque y el bloque, en teoría bien probado y bien entendido no será necesario depurarlo, solo se agregarán los nuevos bloques adicionales en cada extremo siempre y cuando todas sus interfaces de bloques estén bien diseñadas y la funcionalidad bien entendida, puede construir muchas cosas con un mínimo o ningún pegamento. Al igual que con los bloques de lego azul y rojo y negro y amarillo de tamaños y formas conocidas, puedes hacer muchas cosas. siempre y cuando todas sus interfaces de bloques estén bien diseñadas y la funcionalidad bien entendida, puede construir muchas cosas con un mínimo o ningún pegamento. Al igual que con los bloques de lego azul y rojo y negro y amarillo de tamaños y formas conocidas, puedes hacer muchas cosas. siempre y cuando todas sus interfaces de bloques estén bien diseñadas y la funcionalidad bien entendida, puede construir muchas cosas con un mínimo o ningún pegamento. Al igual que con los bloques de lego azul y rojo y negro y amarillo de tamaños y formas conocidas, puedes hacer muchas cosas.

Cada individuo es diferente, su definición de pulido y bien definido y probado y legible varía. No es irracional, por ejemplo, que un profesor dicte las reglas de programación no porque puedan o no ser malas para usted como profesional, sino en algunos casos para facilitar el trabajo de leer y calificar su código para el profesor o los asistentes de estudiantes de posgrado. ... Es igualmente probable, profesionalmente, descubrir que cada trabajo puede tener reglas diferentes por varias razones, por lo general, una o algunas personas en el poder tienen su opinión sobre algo, correcto o incorrecto, y con ese poder pueden dictar que usted lo haga. allí (o renunciar, ser despedido, o de alguna manera llegar al poder). Estas reglas se basan tan a menudo en la opinión como en algún tipo de hecho sobre la legibilidad, el rendimiento, la capacidad de prueba y la portabilidad.

viejo contador de tiempo
fuente
"Por lo tanto, debe encontrar su nivel de comodidad que puede variar y variará para cada programador individual". Creo que el nivel de optimización debe decidirse por los requisitos de rendimiento de cada código, no por lo que le gusta a cada programador.
svick
El compilador seguro, suponiendo que el programador le haya dicho al compilador que optimice los compiladores generalmente por defecto para no optimizar (en la línea de comando el IDE puede tener un valor predeterminado diferente si se usa). Pero el programador puede no saber lo suficiente para optimizar el tamaño de la función y con tantos objetivos ¿dónde se vuelve inútil? El rendimiento de la sintonización manual tiende a tener un efecto negativo en la legibilidad y, en particular, en la mantenibilidad, la comprobabilidad puede ser de cualquier manera.
old_timer
2

Depende de lo inteligente que sea tu compilador. En general, tratar de ser más astuto que el optimizador es una mala idea y en realidad puede privar al compilador de oportunidades para optimizar. Para empezar, probablemente no tengas ni idea de lo que puede hacer y la mayoría de lo que haces realmente influye en lo bien que lo hace.

La optimización prematura es la noción de que los programadores intentan hacer eso y terminan con un código difícil de mantener que en realidad no estaba en la ruta crítica de lo que intentaban hacer. Intentar exprimir la mayor cantidad de CPU posible cuando la mayoría de las veces su aplicación está bloqueada en espera de eventos IO, es algo que veo mucho, por ejemplo.

Lo mejor es codificar la corrección y usar un generador de perfiles para encontrar cuellos de botella en el rendimiento real y corregirlos analizando lo que está en la ruta crítica y si se puede mejorar. A menudo, algunas soluciones simples son muy útiles.

Jilles van Gurp
fuente
0

[elemento de tiempo de computadora]

¿La refactorización hacia un acoplamiento más flexible y funciones más pequeñas afecta la velocidad del código?

Si lo hace. Pero corresponde a los intérpretes, compiladores y compiladores JIT eliminar este código de "costura / cableado", y algunos lo hacen mejor que otros, pero otros no.

La preocupación por múltiples archivos se suma a la sobrecarga de E / S, por lo que eso también afecta bastante la velocidad (en tiempo de computadora).

[elemento de velocidad humana]

(¿Y debería importarme?)

no, no deberías preocuparte. Las computadoras y los circuitos son bastante rápidos en estos días, y otros factores se hacen cargo, como la latencia de la red, la E / S de la base de datos y el almacenamiento en caché.

Entonces, 2x - 4x ralentización en la ejecución del código nativo a menudo se verá ahogada por esos otros factores.

En cuanto a la carga de múltiples archivos, muchas soluciones de almacenamiento en caché se ocupan de eso. Puede llevar más tiempo cargar las cosas y fusionarlas la primera vez, pero para cada próxima vez, para los archivos estáticos, el almacenamiento en caché funciona como si se cargara un solo archivo. El almacenamiento en caché viene como una solución para la carga de múltiples archivos.

Dennis
fuente
0

LA RESPUESTA (en caso de que te la hayas perdido)

Sí, debería preocuparte, pero debes preocuparte por cómo escribes el código y no por el rendimiento.

En breve

No te preocupes por el rendimiento

En el contexto de la pregunta, los compiladores e intérpretes más inteligentes ya se encargan de eso

Cuídate de escribir código mantenible

Código donde los costos de mantenimiento están en el nivel de comprensión humana razonable. es decir, no escriba 1000 funciones más pequeñas que hacen que el código sea incomprensible incluso si comprende cada una de ellas, y no escriba 1 función de objeto divino que sea demasiado grande para comprender, sino que escriba 10 funciones bien diseñadas que tengan sentido para un humano, y que sean facil de mantener.

Dennis
fuente