¿Es buena idea la "metaprogramación" de plantillas en Java?


29

Hay un archivo fuente en un proyecto bastante grande con varias funciones que son extremadamente sensibles al rendimiento (llamadas millones de veces por segundo). De hecho, el mantenedor anterior decidió escribir 12 copias de una función, cada una de las cuales difiere muy ligeramente, para ahorrar el tiempo que se gastaría en verificar los condicionales en una sola función.

Desafortunadamente, esto significa que el código es un PITA para mantener. Me gustaría eliminar todo el código duplicado y escribir solo una plantilla. Sin embargo, el lenguaje Java no admite plantillas, y no estoy seguro de que los genéricos sean adecuados para esto.

Mi plan actual es escribir un archivo que genere las 12 copias de la función (un expansor de plantilla de un solo uso, prácticamente). Por supuesto, proporcionaría una explicación abundante de por qué el archivo debe generarse mediante programación.

Mi preocupación es que esto conduciría a la confusión de los futuros mantenedores, y tal vez introduciría errores desagradables si olvidaban regenerar el archivo después de modificarlo, o (aún peor) si modificaban en su lugar el archivo generado mediante programación. Desafortunadamente, salvo reescribir todo en C ++, no veo forma de solucionarlo.

¿Los beneficios de este enfoque superan las desventajas? ¿Debería en cambio:

  • Tome el golpe de rendimiento y use una única función mantenible.
  • Agregue explicaciones de por qué la función debe duplicarse 12 veces y tome la carga de mantenimiento.
  • Intente usar genéricos como plantillas (probablemente no funcionen de esa manera).
  • Grita al antiguo encargado de mantener el código tan dependiente del rendimiento en una sola función.
  • ¿Otro método para mantener el rendimiento y la mantenibilidad?

PD: Debido al mal diseño del proyecto, perfilar la función es bastante complicado ... sin embargo, el antiguo responsable de mantenimiento me ha convencido de que el impacto en el rendimiento es inaceptable. Asumo que con esto quiere decir más del 5%, aunque eso es una suposición completa de mi parte.


Tal vez debería elaborar un poco. Las 12 copias hacen una tarea muy similar, pero tienen pequeñas diferencias. Las diferencias están en varios lugares a lo largo de la función, por lo que desafortunadamente hay muchas, muchas declaraciones condicionales. Existen efectivamente 6 "modos" de operación y 2 "paradigmas" de operación (palabras hechas por mí mismo). Para usar la función, se especifica el "modo" y el "paradigma" de operación. Esto nunca es dinámico; cada código usa exactamente un modo y paradigma. Los 12 pares de paradigma de modo se usan en algún lugar de la aplicación. Las funciones se denominan adecuadamente func1 a func12, con números pares que representan el segundo paradigma y números impares que representan el primer paradigma.

Soy consciente de que este es el peor diseño de todos si el objetivo es la mantenibilidad. Pero parece ser "lo suficientemente rápido", y este código no ha necesitado ningún cambio durante un tiempo ... También vale la pena señalar que la función original no se ha eliminado (aunque, por lo que puedo decir, es un código muerto) , por lo que refactorizar sería simple.


Si el impacto en el rendimiento es lo suficientemente grave como para garantizar 12 versiones de la función, entonces podría estar atascado con ella. Si refactoriza una sola función (o usa genéricos), ¿el impacto en el rendimiento sería lo suficientemente malo como para perder clientes y negocios?
FrustratedWithFormsDesigner

12
Entonces, creo que necesita hacer sus propias pruebas (sé que dijo que la elaboración de perfiles es complicada, pero aún puede hacer pruebas de rendimiento generales de "caja negra" del sistema en su conjunto, ¿verdad?) Para ver qué tan grande es la diferencia allí es. Y si es notable, entonces creo que podría estar atrapado con el generador de código.
FrustratedWithFormsDesigner

1
Esto podría parecer que podría haberse beneficiado de algún polimorfismo paramétrico (o tal vez incluso genéricos), en lugar de llamar a cada función con un nombre diferente.
Robert Harvey

2
Nombrar las funciones func1 ... func12 parece una locura. Al menos asígneles el nombre mode1Par2, etc. ... o tal vez myFuncM3P2.
user949300

2
"si modifican en su lugar el archivo generado mediante programación" ... cree el archivo solo cuando construya desde " Makefile" (o cualquier sistema que utilice) y elimínelo inmediatamente después de que finalice la compilación . De esta manera, simplemente no tienen la oportunidad de modificar el archivo fuente incorrecto.
Bakuriu

Respuestas:


23

Esta es una situación muy mala, debe refactorizar esto lo antes posible: esta es una deuda técnica en su peor momento, ni siquiera sabe lo importante que es realmente el código , solo especule que es importante.

En cuanto a las soluciones lo antes posible:
algo que se puede hacer es agregar un paso de compilación personalizado. Si usa Maven, que en realidad es bastante simple de hacer, es probable que otros sistemas de compilación automatizados también lo hagan. Escriba un archivo con una extensión diferente a .java y agregue un paso personalizado que busque en su fuente archivos como ese y regenere el .java real. También es posible que desee agregar un gran descargo de responsabilidad en el archivo generado automáticamente que explica que no lo modifique.

Profesionales frente al uso de un archivo generado una vez: sus desarrolladores no obtendrán sus cambios en el .java funcionando. Si realmente ejecutan el código en su máquina antes de comprometerse, descubrirán que sus cambios no tienen ningún efecto (hah). Y luego tal vez leerán el descargo de responsabilidad. Tienes toda la razón al no confiar en tus compañeros de equipo y en ti mismo en el futuro al recordar que este archivo en particular debe cambiarse de una manera diferente. También permite las pruebas automáticas, ya que JUnit compilará su programa antes de ejecutar las pruebas (y también regenerará el archivo)

EDITAR

A juzgar por los comentarios, la respuesta surgió como si fuera una forma de hacer que esto funcione indefinidamente y tal vez sea correcto implementarlo en otras partes críticas del rendimiento de su proyecto.

En pocas palabras: no lo es.

La carga adicional de crear su propio mini-idioma, escribir un generador de códigos para él y mantenerlo, sin mencionar que enseñarlo a futuros mantenedores es infernal a largo plazo. Lo anterior solo permite una forma más segura de manejar el problema mientras trabaja en una solución a largo plazo . Lo que tomará está por encima de mí.


19
Lo siento, pero tengo que estar en desacuerdo. No sabes lo suficiente sobre el código del OP para tomar esta decisión por él. Generar código me parece que contiene más deuda técnica que la solución original.
Robert Harvey

66
El comentario de Robert no puede ser exagerado. Cada vez que tenga un "descargo de responsabilidad que explica no modificar un archivo" que es el equivalente de "deuda técnica" de una tienda de cambio de cheques administrada por un corredor de apuestas ilegal apodado "Tiburón".
corsiKa

8
El archivo generado automáticamente, por supuesto, no pertenece al repositorio de origen (después de todo, no es fuente, es un código compilado) y debe ser eliminado por ant cleano su equivalente. ¡No es necesario poner un descargo de responsabilidad en un archivo que ni siquiera está allí!
Jörg W Mittag

2
@RobertHarvey No estoy tomando ninguna decisión para el OP. OP preguntó si es una buena idea tener esas plantillas en la forma en que las tiene y propuse una (mejor) forma de mantenerlas. No es una solución ideal y, de hecho, como dije en la primera oración, toda la situación es mala, y una forma mucho mejor de avanzar es eliminar el problema, no darle una solución inestable. Eso incluye evaluar adecuadamente cuán crítico es este código, qué tan malo es el impacto en el rendimiento y si hay formas de hacerlo funcionar sin escribir mucho código malo.
Ordous

2
@corsiKa Nunca dije que esta es una solución duradera. Esto sería una deuda tan grande como la situación original. Lo único que hace es disminuir la volatilidad hasta que se encuentre una solución sistemática. Lo que probablemente incluirá pasar por completo a una nueva plataforma / marco / lenguaje ya que si tiene problemas como este en Java, está haciendo algo extremadamente complejo o extremadamente incorrecto.
Ordous

16

¿El mantenimiento es real o solo te molesta? Si solo te molesta, déjalo en paz.

¿El problema de rendimiento es real o el desarrollador anterior solo pensó que lo era? Los problemas de rendimiento, la mayoría de las veces, no están donde se cree que están, incluso cuando se utiliza un generador de perfiles. (Utilizo esta técnica para encontrarlos de manera confiable).

Por lo tanto, existe la posibilidad de que tenga una solución fea para un problema, pero también podría ser una solución fea para un problema real. En caso de duda, déjalo en paz.


10

Realmente asegúrese de que en condiciones reales de producción (consumo de memoria realista, que desencadena la recolección de basura de manera realista, etc.), todos esos métodos separados realmente hacen una diferencia de rendimiento (en lugar de tener un solo método). Esto tomará unos días de su tiempo, pero puede ahorrarle semanas simplificando el código con el que trabaja a partir de ahora.

Además, si lo hace descubre que necesita todas las funciones, es posible que desee utilizar Javassist para generar el código de programación. Como otros han señalado, puede automatizarlo con Maven .


2
Además, incluso si resulta que los problemas de rendimiento son reales, el ejercicio de haberlos estudiado lo ayudará a saber mejor qué es posible con su código.
Carl Manaster

6

¿Por qué no incluir algún tipo de preprocesador de plantilla \ generación de código para este sistema? Un paso de compilación adicional personalizado que ejecuta y emite archivos fuente java adicionales antes de compilar el resto del código. Así es como a menudo funciona incluir clientes web fuera de los archivos wsdl y xsd.

Por supuesto, tendrá el mantenimiento de ese preprocesador \ generador de código, pero no tendrá que preocuparse por el mantenimiento duplicado del código central.

Dado que se emite el código Java en tiempo de compilación, no se debe pagar una penalización de rendimiento por código adicional. Pero obtiene una simplificación de mantenimiento al tener la plantilla en lugar de todo el código duplicado.

Los genéricos de Java no brindan ningún beneficio de rendimiento debido a la eliminación de tipos en el lenguaje, se utiliza una conversión simple en su lugar.

Desde mi comprensión de las plantillas de C ++, se compilan en varias funciones por cada invocación de plantilla. Terminará con un código de tiempo de compilación media duplicado para cada tipo que almacene en un std :: vector, por ejemplo.


2

Ningún método Java sensato puede ser lo suficientemente largo como para tener 12 variantes ... y JITC odia los métodos largos, simplemente se niega a optimizarlos adecuadamente. He visto un factor de aceleración de dos simplemente dividiendo un método en dos más cortos. Tal vez este es el camino a seguir.

OTOH tener múltiples copias puede tener sentido incluso si hubiera idénticas. A medida que cada uno de ellos se usa en diferentes lugares, se optimizan para diferentes casos (el JITC los perfila y luego coloca los casos raros en un camino excepcional.

Yo diría que generar código no es gran cosa, suponiendo que haya una buena razón. La sobrecarga es bastante baja, y los archivos con nombre adecuado conducen inmediatamente a su origen. Hace mucho tiempo, cuando generé el código fuente, puse // DO NOT EDITen cada línea ... Supongo que eso es suficiente.


1

No hay absolutamente nada de malo en el "problema" que mencionó. Por lo que sé, este es el tipo exacto de diseño y enfoque utilizado por el servidor DB para tener un buen rendimiento.

Tienen muchos métodos especiales para asegurarse de que pueden maximizar el rendimiento para todo tipo de operaciones: unir, seleccionar, agregar, etc., cuando se aplican ciertas condiciones.

En resumen, la generación de código como el que crees que es una mala idea. Quizás debería mirar este diagrama para ver cómo un DB resuelve un problema similar al suyo:ingrese la descripción de la imagen aquí


1

Es posible que desee verificar si aún puede usar algunas abstracciones sensibles y usar, por ejemplo, el patrón del método de plantilla para escribir código comprensible para la funcionalidad común y mover las diferencias entre los métodos a las "operaciones primitivas" (de acuerdo con la descripción del patrón) en 12 subclases Esto mejorará enormemente la capacidad de mantenimiento y la capacidad de prueba, y en realidad podría tener el mismo rendimiento que el código actual, ya que la JVM puede alinear las llamadas de método para las operaciones primitivas después de un tiempo. Por supuesto, debe verificar esto en una prueba de rendimiento.


0

Puede resolver el problema de los métodos especializados utilizando el lenguaje Scala.

Scala puede incluir métodos en línea, esto (en combinación con el uso fácil de funciones de orden superior) hace posible evitar la duplicación de código de forma gratuita; este parece ser el problema principal mencionado en su respuesta.

Pero también, Scala tiene macros sintácticas, lo que hace posible hacer muchas cosas con código en tiempo de compilación de una manera segura.

Y el problema común de los tipos primitivos de boxeo cuando se usan en genéricos también es posible de resolver en Scala: puede hacer una especialización genérica para los primitivos para evitar el boxeo automáticamente mediante el uso de @specializedanotaciones; esto está integrado en el lenguaje mismo. Entonces, básicamente, escribirás un método genérico en Scala, y se ejecutará con la velocidad de los métodos especializados. Y si también necesita una aritmética genérica rápida, es fácilmente factible mediante el uso de "patrón" de Typeclass para inyectar las operaciones y los valores para diferentes tipos numéricos.

Además, Scala es impresionante en muchas otras formas. Y no tiene que volver a escribir todo el código en Scala, porque la interoperabilidad Scala <-> Java es excelente. Solo asegúrese de usar SBT (herramienta de construcción scala) para construir el proyecto.


-1

Si la función en cuestión es grande, convertir los bits de "modo / paradigma" en una interfaz y luego pasar un objeto que implemente esa interfaz como parámetro a la función puede funcionar. GoF llama a esto el patrón "Estrategia", iirc. Si la función es pequeña, el aumento de los gastos generales puede ser significativo ... ¿Alguien mencionó el perfil todavía? ... Más personas deberían mencionar el perfil.


esto se lee más como un comentario que como una respuesta
mosquito

-2

¿Cuántos años tiene el programa? Probablemente el hardware más nuevo eliminaría el cuello de botella y podría cambiar a una versión más fácil de mantener. Pero debe haber una razón para el mantenimiento, por lo que, a menos que su tarea sea mejorar la base de código, déjelo como está funcionando.


1
esto parece simplemente repetir los puntos realizados y explicados en una respuesta antes publicado hace pocas horas
mosquito

1
La única forma de determinar dónde está realmente un cuello de botella es perfilarlo, como se menciona en la otra respuesta. Sin esa información clave, cualquier solución sugerida para el rendimiento es pura especulación.
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.