Le modèle «métaprogrammation» en Java est-il une bonne idée?

29

Il existe un fichier source dans un projet assez volumineux avec plusieurs fonctions extrêmement sensibles aux performances (appelées des millions de fois par seconde). En fait, le responsable précédent a décidé d'écrire 12 copies d'une fonction chacune différant très légèrement, afin de gagner du temps qui serait consacré à la vérification des conditions dans une seule fonction.

Malheureusement, cela signifie que le code est un PITA à maintenir. Je voudrais supprimer tout le code en double et écrire un seul modèle. Cependant, le langage Java ne prend pas en charge les modèles et je ne suis pas sûr que les génériques conviennent à cela.

Mon plan actuel est d'écrire à la place un fichier qui génère les 12 copies de la fonction (un expanseur de modèle à usage unique, pratiquement). Je fournirais bien sûr de nombreuses explications pour lesquelles le fichier doit être généré par programme.

Ma préoccupation est que cela conduirait à la confusion des futurs responsables, et introduirait peut-être des bugs désagréables s'ils oublient de régénérer le fichier après l'avoir modifié, ou (pire encore) s'ils modifient à la place le fichier généré par programme. Malheureusement, à moins de réécrire le tout en C ++, je ne vois aucun moyen de résoudre ce problème.

Les avantages de cette approche l'emportent-ils sur les inconvénients? Dois-je plutôt:

  • Profitez des performances et utilisez une seule fonction maintenable.
  • Ajoutez des explications sur les raisons pour lesquelles la fonction doit être dupliquée 12 fois et supportez gracieusement la charge de maintenance.
  • Essayez d'utiliser des génériques comme modèles (ils ne fonctionnent probablement pas de cette façon).
  • Criez à l'ancien mainteneur pour avoir rendu le code si dépendant des performances d'une seule fonction.
  • Autre méthode pour maintenir les performances et la maintenabilité?

PS En raison de la mauvaise conception du projet, le profilage de la fonction est plutôt délicat ... cependant, l'ancien mainteneur m'a convaincu que le niveau de performance est inacceptable. Je suppose qu'il entend par là plus de 5%, bien que ce soit une supposition complète de ma part.


Je devrais peut-être élaborer un peu. Les 12 copies font une tâche très similaire, mais ont des différences infimes. Les différences se trouvent à divers endroits de la fonction, donc malheureusement, il existe de très nombreuses instructions conditionnelles. Il y a effectivement 6 "modes" de fonctionnement, et 2 "paradigmes" de fonctionnement (mots inventés par moi-même). Pour utiliser la fonction, on précise le "mode" et le "paradigme" de fonctionnement. Ce n'est jamais dynamique; chaque morceau de code utilise exactement un mode et un paradigme. Les 12 paires mode-paradigme sont utilisées quelque part dans l'application. Les fonctions sont bien nommées func1 à func12, les nombres pairs représentant le deuxième paradigme et les nombres impairs représentant le premier paradigme.

Je suis conscient que c'est à peu près la pire conception jamais réalisée si la maintenabilité est l'objectif. Mais il semble être "assez rapide", et ce code n'a pas eu besoin de modifications depuis un moment ... Il convient également de noter que la fonction d'origine n'a pas été supprimée (bien qu'il s'agisse d'un code mort pour autant que je sache) , le refactoring serait donc simple.

Fengyang Wang
la source
Si la performance est suffisamment sérieuse pour justifier 12 versions de la fonction, alors vous pourriez être coincé avec elle. Si vous refactorisez une seule fonction (ou utilisez des génériques), les performances seront-elles suffisamment mauvaises pour que vous perdiez des clients et des affaires?
FrustratedWithFormsDesigner
12
Ensuite, je pense que vous devez faire vos propres tests (je sais que vous avez dit que le profilage est délicat, mais vous pouvez toujours faire des tests de performance "boîte noire" approximatifs du système dans son ensemble, non?) Pour voir à quel point la différence est grande est. Et si c'est perceptible, alors je pense que vous pourriez être coincé avec le générateur de code.
FrustratedWithFormsDesigner
1
Cela pourrait sembler avoir pu bénéficier d'un certain polymorphisme paramétrique (ou peut-être même de génériques), plutôt que d'appeler chaque fonction par un nom différent.
Robert Harvey
2
Nommer les fonctions func1 ... func12 semble fou. Nommez-les au moins mode1Par2 etc ... ou peut-être myFuncM3P2.
user949300
2
"s'ils modifient à la place le fichier généré par programme" ... créez le fichier uniquement lors de la construction à partir du " Makefile" (ou du système que vous utilisez) et supprimez-le juste après la compilation . De cette façon, ils n'ont tout simplement pas la possibilité de modifier le mauvais fichier source.
Bakuriu

Réponses:

23

C'est une très mauvaise situation, vous devez refactoriser ce DÈS QUE POSSIBLE - c'est une dette technique dans le pire - vous ne savez même pas à quel point le code est vraiment important - spéculez seulement qu'il est important.

Quant aux solutions DÈS QUE POSSIBLE:
quelque chose qui peut être fait est d'ajouter une étape de compilation personnalisée. Si vous utilisez Maven qui est en fait assez simple à faire, d'autres systèmes de construction automatisés sont susceptibles de faire face à cela également. Écrivez un fichier avec une extension différente de .java et ajoutez une étape personnalisée qui recherche des fichiers comme celui-ci dans votre source et régénère le .java réel. Vous pouvez également ajouter un énorme avertissement sur le fichier généré automatiquement expliquant de ne pas le modifier.

Avantages par rapport à l'utilisation d'un fichier généré une fois: vos développeurs ne verront pas leurs modifications au travail .java fonctionner. S'ils exécutent réellement le code sur leur machine avant de valider, ils constateront que leurs modifications n'ont aucun effet (hah). Et puis peut-être qu'ils liront l'avertissement. Vous avez absolument raison de ne pas faire confiance à vos coéquipiers et à votre avenir en vous rappelant que ce fichier particulier doit être changé d'une manière différente. Il permet également des tests automatiques comme wel, car JUnit compilera votre programme avant d'exécuter des tests (et régénérera également le fichier)

MODIFIER

À en juger par les commentaires, la réponse est apparue comme si c'était un moyen de faire fonctionner indéfiniment et peut-être OK de déployer sur d'autres parties critiques de la performance de votre projet.

Autrement dit: ce n'est pas le cas.

La charge supplémentaire de créer votre propre mini-langage, d'écrire un générateur de code pour celui-ci et de le maintenir, sans parler de l'enseigner aux futurs responsables, est infernale à long terme. Ce qui précède ne permet qu'une manière plus sûre de gérer le problème pendant que vous travaillez sur une solution à long terme . Ce que cela prendra est au-dessus de moi.

Ordous
la source
19
Désolé, mais je dois être en désaccord. Vous ne connaissez pas assez le code du PO pour prendre cette décision pour lui. Générer du code me semble qu'il contient plus de dettes techniques que la solution d'origine.
Robert Harvey
6
Le commentaire de Robert ne peut pas être surestimé. Chaque fois que vous avez un "avertissement expliquant de ne pas modifier un fichier" qui est l'équivalent de la "dette technique" d'un magasin d'encaissement de chèques géré par un bookmaker illégal surnommé "Shark".
corsiKa
8
Bien entendu, le fichier généré automatiquement n'appartient pas au référentiel source (il n'est pas source, après tout, c'est du code compilé) et il doit être supprimé par ant cleanou quel que soit votre équivalent. Pas besoin de mettre un avis de non-responsabilité dans un fichier qui n'est même pas là!
Jörg W Mittag
2
@RobertHarvey Je ne prends aucune décision pour l'OP. OP a demandé si c'était une bonne idée d'avoir de tels modèles dans la façon dont il les avait et j'ai proposé une (meilleure) façon de les maintenir. Ce n'est pas une solution idéale et, en fait, comme je l'ai dit dans la toute première phrase, toute la situation est mauvaise, et une bien meilleure façon d'aller de l'avant est d'éliminer le problème, pas d'y apporter une solution instable. Cela inclut d'évaluer correctement à quel point ce code est critique, à quel point les performances sont affectées et s'il existe des moyens de le faire fonctionner sans écrire beaucoup de mauvais code.
Ordous
2
@corsiKa Je n'ai jamais dit que c'était une solution durable. Ce serait autant une dette que la situation d'origine. La seule chose qu'il fait est de diminuer la volatilité jusqu'à ce qu'une solution systématique soit trouvée. Ce qui impliquera probablement de passer entièrement à une nouvelle plate-forme / infrastructure / langage, car si vous rencontrez des problèmes comme celui-ci en Java - vous faites quelque chose d'extrêmement complexe ou extrêmement mal.
Ordous
16

L'entretien est-il réel ou vous dérange-t-il simplement? Si cela vous dérange, laissez-le tranquille.

Le problème de performances est-il réel ou le développeur précédent pensait- il seulement que c'était le cas? Les problèmes de performances, le plus souvent, ne sont pas là où ils sont censés être, même lorsqu'un profileur est utilisé. (J'utilise cette technique pour les trouver de manière fiable.)

Il est donc possible que vous ayez une solution laide à un non-problème, mais cela pourrait également être une solution laide à un problème réel. En cas de doute, laissez-le tranquille.

Mike Dunlavey
la source
10

Assurez-vous vraiment que dans des conditions réelles de production (consommation de mémoire réaliste, qui déclenche le garbage collection de manière réaliste, etc.), toutes ces méthodes distinctes font vraiment une différence de performance (par opposition à une seule méthode). Cela prendra quelques jours de votre temps, mais peut vous faire gagner des semaines en simplifiant le code avec lequel vous travaillez à partir de maintenant.

En outre, si vous faites découvrir que vous avez besoin de toutes les fonctions, vous pouvez utiliser Javassist pour générer le code programatically. Comme d'autres l'ont souligné, vous pouvez l'automatiser avec Maven .

Shivan Dragon
la source
2
De plus, même s'il s'avère que les problèmes de performances sont réels, l'exercice de les avoir étudiés vous aidera à mieux savoir ce qui est possible avec votre code.
Carl Manaster
6

Pourquoi ne pas inclure une sorte de préprocesseur de modèle \ génération de code pour ce système? Une étape de construction supplémentaire personnalisée qui exécute et émet des fichiers source java supplémentaires avant de compiler le reste du code. C'est ainsi que l'inclusion de clients Web hors des fichiers wsdl et xsd fonctionne souvent.

Vous aurez bien sûr la maintenance de ce générateur de préprocesseur \ code, mais vous n'aurez pas à vous soucier de la maintenance du code de base dupliqué.

Étant donné que le code Java au moment de la compilation est émis, il n'y a pas de pénalité de performance à payer pour le code supplémentaire. Mais vous gagnez en simplification de la maintenance en ayant le modèle au lieu de tout le code dupliqué.

Les génériques Java n'apportent aucun avantage en termes de performances en raison de l'effacement des types dans le langage, une simple conversion est utilisée à sa place.

D'après ma compréhension des modèles C ++, ils sont compilés en plusieurs fonctions pour chaque invocation de modèle. Vous vous retrouverez avec un code temporel à mi-compilation en double pour chaque type que vous stockez dans un vecteur std :: par exemple.

Peter Smith
la source
2

Aucune méthode Java sensée ne peut être assez longue pour avoir 12 variantes ... et JITC déteste les méthodes longues - il refuse simplement de les optimiser correctement. J'ai vu un facteur d'accélération de deux en divisant simplement une méthode en deux plus courtes. C'est peut-être la voie à suivre.

OTOH ayant plusieurs copies peut avoir du sens même si elles étaient identiques. Au fur et à mesure que chacun d'eux est utilisé à différents endroits, ils sont optimisés pour différents cas (le JITC les profile puis place les cas rares sur un chemin exceptionnel.

Je dirais que générer du code n'est pas un gros problème, en supposant qu'il y a une bonne raison. La surcharge est plutôt faible et les fichiers correctement nommés mènent immédiatement à leur source. Il y a longtemps que j'ai généré du code source, j'ai mis // DO NOT EDITsur chaque ligne ... Je suppose que c'est assez d'économiser.

maaartinus
la source
1

Il n'y a absolument rien de mal avec le «problème» que vous avez mentionné. D'après ce que je sais, c'est le type exact de conception et d'approche utilisé par le serveur DB pour avoir de bonnes performances.

Ils ont beaucoup de méthodes spéciales pour s'assurer qu'ils peuvent maximiser les performances pour toutes sortes d'opérations: joindre, sélectionner, agréger, etc. lorsque certaines conditions s'appliquent.

En bref, la génération de code comme celle que vous pensez est une mauvaise idée. Vous devriez peut-être regarder ce diagramme pour voir comment une base de données résout un problème similaire au vôtre:entrez la description de l'image ici

randomA
la source
1

Vous voudrez peut-être vérifier si vous pouvez toujours utiliser des abstractions sensées et utiliser par exemple le modèle de méthode de modèle pour écrire du code compréhensible pour la fonctionnalité commune et déplacer les différences entre les méthodes dans les "opérations primitives" (selon la description du modèle) dans 12 sous-classes. Cela améliorera considérablement la maintenabilité et la testabilité, et il pourrait en fait avoir les mêmes performances que le code actuel, car la JVM peut incorporer les appels de méthode pour les opérations primitives après un certain temps. Bien sûr, vous devez vérifier cela dans un test de performance.

Hans-Peter Störr
la source
0

Vous pouvez résoudre le problème des méthodes spécialisées en utilisant le langage Scala.

Scala peut intégrer des méthodes, cela (en combinaison avec une utilisation facile des fonctions d'ordre supérieur) permet d'éviter la duplication de code gratuitement - cela ressemble au principal problème mentionné dans votre réponse.

Mais aussi, Scala a des macros syntaxiques, ce qui permet de faire beaucoup de choses avec du code au moment de la compilation de manière sécurisée.

Et le problème commun des types de primitives de boxe lorsqu'ils sont utilisés dans des génériques est également possible à résoudre dans Scala: il peut faire une spécialisation générique pour les primitives pour éviter la boxe automatiquement en utilisant l' @specializedannotation - cette chose est intégrée dans le langage lui-même. Donc, en gros, vous écrirez une méthode générique dans Scala, et elle fonctionnera à la vitesse des méthodes spécialisées. Et si vous avez également besoin d'arithmétiques génériques rapides, cela est facilement réalisable en utilisant le "modèle" Typeclass pour injecter les opérations et les valeurs pour différents types numériques.

En outre, Scala est génial à bien d'autres égards. Et vous n'avez pas à réécrire tout le code dans Scala, car l'interopérabilité Java Scala <-> est excellente. Assurez-vous simplement d'utiliser SBT (scala build tool) pour construire le projet.

Sarge Borsch
la source
-1

Si la fonction en question est volumineuse, transformer les bits "mode / paradigme" en une interface, puis passer un objet qui implémente cette interface en tant que paramètre à la fonction peut fonctionner. Le GoF appelle cela le modèle de «stratégie», iirc. Si la fonction est petite, les frais généraux accrus peuvent être importants ... quelqu'un a-t-il déjà mentionné le profilage? ... davantage de personnes devraient mentionner le profilage.

mjfgates
la source
cela ressemble plus à un commentaire qu'à une réponse
moucher
-2

Quel âge a le programme? Le matériel le plus récent supprimerait probablement le goulot d'étranglement et vous pourriez passer à une version plus maintenable. Mais il doit y avoir une raison pour la maintenance, donc à moins que votre tâche ne soit d'améliorer la base de code, laissez-la telle qu'elle fonctionne.

user138623
la source
1
cela semble simplement répéter les points avancés et expliqués dans une réponse précédente publiée il y a quelques heures
gnat
1
La seule façon de déterminer où se trouve réellement un goulot d'étranglement est de le profiler - comme mentionné dans l'autre réponse. Sans ces informations clés, toute correction suggérée des performances est de la pure spéculation.