Ist das „Metaprogrammieren“ von Vorlagen in Java eine gute Idee?


29

In einem relativ großen Projekt befindet sich eine Quelldatei mit mehreren Funktionen, die extrem leistungsabhängig sind (millionenfach pro Sekunde). Tatsächlich hat der vorherige Betreuer beschlossen, 12 Kopien einer Funktion zu schreiben, von denen sich jede geringfügig unterscheidet, um die Zeit zu sparen, die für die Prüfung der Bedingungen in einer einzelnen Funktion aufgewendet werden würde.

Leider bedeutet dies, dass der Code eine zu pflegende PITA ist. Ich möchte den gesamten doppelten Code entfernen und nur eine Vorlage schreiben. Die Sprache Java unterstützt jedoch keine Vorlagen, und ich bin nicht sicher, ob Generika dafür geeignet sind.

Mein aktueller Plan ist es, stattdessen eine Datei zu schreiben, die die 12 Kopien der Funktion generiert (praktisch ein Einweg-Template-Expander). Ich würde natürlich ausführlich erklären, warum die Datei programmgesteuert generiert werden muss.

Ich befürchte, dass dies zu Verwirrung bei zukünftigen Betreuern führen und möglicherweise böse Fehler verursachen könnte, wenn diese vergessen, die Datei nach der Änderung neu zu generieren, oder (noch schlimmer), wenn sie stattdessen die programmgesteuert generierte Datei ändern. Leider sehe ich keine Möglichkeit, dies zu beheben, da ich das Ganze nicht in C ++ neu geschrieben habe.

Überwiegen die Vorteile dieses Ansatzes die Nachteile? Soll ich stattdessen:

  • Nehmen Sie den Leistungstreffer und nutzen Sie eine einzige wartbare Funktion.
  • Fügen Sie Erklärungen hinzu, warum die Funktion 12-mal dupliziert werden muss, und nehmen Sie die Wartungslast auf sich.
  • Versuchen Sie, Generika als Vorlagen zu verwenden (wahrscheinlich funktionieren sie nicht so).
  • Schreien Sie den alten Betreuer an, der den Code so leistungsabhängig von einer einzelnen Funktion gemacht hat.
  • Andere Methode zur Aufrechterhaltung der Leistung und Wartbarkeit?

PS Aufgrund des schlechten Designs des Projekts ist das Profilieren der Funktion ziemlich schwierig. Der frühere Betreuer hat mich jedoch davon überzeugt, dass der Leistungseinbruch inakzeptabel ist. Ich nehme an, dass er damit mehr als 5% meint, obwohl das eine vollständige Vermutung von meiner Seite ist.


Vielleicht sollte ich etwas näher darauf eingehen. Die 12 Exemplare haben eine sehr ähnliche Aufgabe, weisen jedoch winzige Unterschiede auf. Die Unterschiede sind an verschiedenen Stellen in der Funktion zu finden, so dass es leider viele, viele Bedingungsaussagen gibt. Es gibt effektiv 6 "Betriebsarten" und 2 "Paradigmen" (Wörter, die ich selbst zusammengestellt habe). Um die Funktion zu verwenden, gibt man den "Modus" und das "Paradigma" des Betriebs an. Das ist niemals dynamisch; Jeder Code verwendet genau einen Modus und ein Paradigma. Alle 12 Mode-Paradigma-Paare werden irgendwo in der Anwendung verwendet. Die Funktionen haben die passenden Namen func1 bis func12, wobei gerade Zahlen das zweite Paradigma und ungerade Zahlen das erste Paradigma darstellen.

Mir ist bewusst, dass dies das schlechteste Design überhaupt ist, wenn Wartbarkeit das Ziel ist. Aber es scheint "schnell genug" zu sein, und dieser Code hat eine Weile keine Änderungen nötig ... Es ist auch erwähnenswert, dass die ursprüngliche Funktion nicht gelöscht wurde (obwohl es toter Code ist, soweit ich das beurteilen kann) , so wäre Refactoring einfach.


Wenn der Leistungseinbruch schwerwiegend genug ist, um 12 Versionen der Funktion zu rechtfertigen, stecken Sie möglicherweise fest. Wenn Sie eine einzelne Funktion überarbeiten (oder Generika verwenden), wäre die Leistung so schlecht, dass Sie Kunden und Unternehmen verlieren?
FrustratedWithFormsDesigner

12
Ich denke, Sie müssen Ihre eigenen Tests durchführen (ich weiß, Sie sagten, die Profilerstellung sei schwierig, aber Sie können immer noch grobe Black-Box-Leistungstests des gesamten Systems durchführen, oder?), Um zu sehen, wie groß der Unterschied ist ist. Und wenn es auffällt, dann denke ich, dass Sie vielleicht beim Codegenerator hängen bleiben.
FrustratedWithFormsDesigner

1
Dies könnte so klingen, als hätte es von einem parametrischen Polymorphismus (oder vielleicht sogar von Generika) profitiert, anstatt jede Funktion unter einem anderen Namen aufzurufen.
Robert Harvey

2
Die Funktionen func1 ... func12 zu benennen, scheint verrückt. Nennen Sie sie mindestens mode1Par2 etc ... oder vielleicht myFuncM3P2.
user949300

2
"Wenn sie stattdessen die programmgesteuert generierte Datei ändern" ... erstellen Sie die Datei nur, wenn Sie sie vom " Makefile" (oder einem anderen von Ihnen verwendeten System) erstellen und direkt nach Abschluss der Kompilierung entfernen . Auf diese Weise haben sie einfach keine Chance, die falsche Quelldatei zu ändern.
Bakuriu

Antworten:


23

Dies ist eine sehr schlechte Situation. Sie müssen diese so schnell wie möglich überarbeiten. Dies ist eine technische Verschuldung. Sie wissen nicht einmal, wie wichtig der Code wirklich ist. Spekulieren Sie nur, dass er wichtig ist.

Lösungsvorschläge: Es
kann ein benutzerdefinierter Kompilierungsschritt hinzugefügt werden. Wenn Sie Maven verwenden, das eigentlich ziemlich einfach zu tun ist, werden andere automatisierte Build-Systeme wahrscheinlich auch damit fertig. Schreiben Sie eine Datei mit einer anderen Erweiterung als .java und fügen Sie einen benutzerdefinierten Schritt hinzu, der Ihre Quelle nach solchen Dateien durchsucht und die eigentliche .java-Datei neu generiert. Möglicherweise möchten Sie der automatisch generierten Datei auch einen umfangreichen Haftungsausschluss hinzufügen, in dem erläutert wird, dass sie nicht geändert werden darf .

Vorteile gegenüber der Verwendung einer einmal generierten Datei: Ihre Entwickler werden ihre Änderungen an der .java-Datei nicht zum Laufen bringen. Wenn sie den Code tatsächlich vor dem Festschreiben auf ihrem Computer ausführen, stellen sie fest, dass ihre Änderungen keine Auswirkungen haben (hah). Und dann lesen sie vielleicht den Haftungsausschluss. Sie haben absolut Recht, Ihren Teamkollegen und Ihrem zukünftigen Selbst nicht zu vertrauen, wenn Sie sich daran erinnern, dass diese bestimmte Datei auf eine andere Weise geändert werden muss. Es ermöglicht auch das automatische Testen, da JUnit Ihr Programm kompiliert, bevor Tests ausgeführt werden (und die Datei ebenfalls neu generiert).

BEARBEITEN

Gemessen an den Kommentaren war die Antwort so, als ob dies eine Möglichkeit wäre, diese Funktion auf unbestimmte Zeit zu nutzen und sie möglicherweise für andere leistungskritische Teile Ihres Projekts bereitzustellen.

Einfach ausgedrückt: es ist nicht so.

Der zusätzliche Aufwand, eine eigene Mini-Sprache zu erstellen, einen Code-Generator dafür zu schreiben und zu pflegen, sowie zukünftige Betreuer zu unterrichten, ist auf lange Sicht höllisch. Das oben Genannte bietet nur eine sichere Möglichkeit, das Problem zu lösen, während Sie an einer langfristigen Lösung arbeiten . Was das braucht, ist über mir.


19
Entschuldigung, aber ich muss nicht zustimmen. Sie wissen nicht genug über den OP-Code, um diese Entscheidung für ihn zu treffen. Das Erzeugen von Code scheint mir mehr technische Schulden zu enthalten als die ursprüngliche Lösung.
Robert Harvey

6
Roberts Kommentar kann nicht übertrieben werden. Jedes Mal, wenn Sie einen "Haftungsausschluss" haben, der erklärt, dass eine Datei nicht geändert werden darf, handelt es sich um die "technische Schuld" eines Scheckeinlösungsgeschäfts, das von einem illegalen Buchmacher mit dem Spitznamen "Shark" betrieben wird.
corsiKa

8
Die automatisch generierte Datei gehört natürlich nicht in das Quell-Repository (es ist schließlich keine Quelle, es ist kompilierter Code) und sollte entfernt werden von ant cleanoder was auch immer Ihre Entsprechung ist. Sie müssen keinen Haftungsausschluss in eine Datei einfügen, die noch nicht einmal vorhanden ist!
Jörg W Mittag

2
@RobertHarvey Ich treffe keine Entscheidungen für das OP. OP fragte, ob es eine gute Idee sei, solche Vorlagen so zu haben, wie er sie hat, und ich schlug eine (bessere) Möglichkeit vor, sie zu pflegen. Es ist keine ideale Lösung, und in der Tat ist, wie ich bereits im ersten Satz ausgeführt habe, die gesamte Situation schlecht, und es ist viel besser, das Problem zu beseitigen, als eine unsichere Lösung zu finden. Dazu gehört die richtige Beurteilung, wie kritisch dieser Code ist, wie schlecht die Leistung ist und ob es Möglichkeiten gibt, ihn zum Laufen zu bringen, ohne viel schlechten Code zu schreiben.
Ordous

2
@corsiKa Ich habe nie gesagt, dass dies eine dauerhafte Lösung ist. Dies wäre ebenso eine Verschuldung wie die ursprüngliche Situation. Das einzige, was es tut, ist die Verringerung der Flüchtigkeit, bis eine systematische Lösung gefunden wird. Dazu gehört wahrscheinlich die vollständige Umstellung auf eine neue Plattform / ein neues Framework / eine neue Sprache, da Sie, wenn Sie in Java auf Probleme wie dieses stoßen, entweder etwas extrem Komplexes oder extrem Falsches tun.
Ordous

16

Ist die Wartung wirklich erfolgreich oder stört es Sie nur? Wenn es Sie nur stört, lassen Sie es in Ruhe.

Ist das Leistungsproblem real oder hat der vorherige Entwickler nur geglaubt, dass es das war? Leistungsprobleme treten meistens nicht dort auf, wo sie vermutet werden, auch wenn ein Profiler verwendet wird. (Ich benutze diese Technik , um sie zuverlässig zu finden.)

Es besteht also die Möglichkeit, dass Sie eine hässliche Lösung für ein Nicht-Problem haben, aber es könnte auch eine hässliche Lösung für ein echtes Problem sein. Wenn Sie Zweifel haben, lassen Sie es in Ruhe.


10

Vergewissern Sie sich wirklich, dass all diese getrennten Methoden unter realen Produktionsbedingungen (realistischer Speicherverbrauch, der die Garbage Collection realistisch auslöst, usw.) tatsächlich einen Leistungsunterschied bewirken (im Gegensatz zu nur einer Methode). Dies wird einige Tage Ihrer Zeit in Anspruch nehmen, kann Ihnen jedoch Wochen ersparen, indem Sie den Code, mit dem Sie von nun an arbeiten, vereinfachen.

Auch, wenn Sie tun entdecken , dass Sie alle Funktionen benötigen, sollten Sie verwenden Javassist den Code programmatisch zu erzeugen. Wie bereits erwähnt, können Sie dies mit Maven automatisieren .


2
Auch wenn sich herausstellt, dass die Leistungsprobleme real sind, hilft Ihnen die Übung, sie studiert zu haben, besser zu wissen, was mit Ihrem Code möglich ist.
Carl Manaster

6

Warum nicht irgendeine Art von Template-Präprozessor / Code-Generierung für dieses System einbinden? Ein benutzerdefinierter zusätzlicher Build-Schritt, der zusätzliche Java-Quelldateien ausführt und ausgibt, bevor der Rest des Codes kompiliert wird. So funktioniert es oft, Web-Clients von wsdl- und xsd-Dateien einzuschließen.

Sie haben natürlich die Wartung dieses Präprozessors \ Codegenerators, aber Sie müssen sich nicht um die doppelte Kerncodewartung kümmern.

Da zur Kompilierungszeit Java-Code ausgegeben wird, ist für zusätzlichen Code keine Leistungseinbuße zu zahlen. Sie erhalten jedoch eine Vereinfachung der Wartung, indem Sie die Vorlage anstelle des gesamten duplizierten Codes verwenden.

Java-Generika bieten keinen Leistungsvorteil, da der Typ in der Sprache gelöscht wird. Stattdessen wird einfaches Casting verwendet.

Nach meinem Verständnis der C ++ - Vorlagen werden sie für jeden Vorlagenaufruf in mehrere Funktionen kompiliert. Sie erhalten doppelten Mid-Compile-Zeitcode für jeden Typ, den Sie beispielsweise in einem std :: vector speichern.


2

Keine vernünftige Java-Methode kann lang genug sein, um 12 Varianten zu haben ... und JITC hasst lange Methoden - es weigert sich einfach, sie richtig zu optimieren. Ich habe einen Beschleunigungsfaktor von zwei gesehen, indem ich einfach eine Methode in zwei kürzere aufgeteilt habe. Vielleicht ist dies der richtige Weg.

OTOH mit mehreren Kopien kann sinnvoll sein, auch wenn es identisch wäre. Da jeder von ihnen an verschiedenen Orten verwendet wird, werden sie für verschiedene Fälle optimiert (das JITC profiliert sie und legt die seltenen Fälle dann auf einen außergewöhnlichen Pfad.

Ich würde sagen, dass das Generieren von Code keine große Sache ist, vorausgesetzt, es gibt einen guten Grund. Der Overhead ist eher gering und richtig benannte Dateien führen sofort zu ihrer Quelle. Vor einiger Zeit, als ich Quellcode generiert habe, habe ich // DO NOT EDITjede Zeile eingefügt ... Ich denke, das ist sicher genug.


1

An dem von Ihnen angesprochenen "Problem" ist absolut nichts auszusetzen. Soweit ich weiß, handelt es sich hierbei genau um die Art und Weise, wie der DB-Server eine gute Leistung erzielt.

Sie haben viele spezielle Methoden, um sicherzustellen, dass sie die Leistung für alle Arten von Operationen maximieren können: Verbinden, Auswählen, Zusammenfassen usw., wenn bestimmte Bedingungen zutreffen.

Kurz gesagt, Code-Generierung wie die, die Sie für eine schlechte Idee halten. Vielleicht sollten Sie sich dieses Diagramm ansehen, um zu sehen, wie eine Datenbank ein ähnliches Problem löst:Bildbeschreibung hier eingeben


1

Vielleicht möchten Sie überprüfen, ob Sie noch einige sinnvolle Abstraktionen verwenden können, und z. B. mithilfe des Schablonenmethodenmusters verständlichen Code für die allgemeine Funktionalität schreiben und die Unterschiede zwischen den Methoden in die "primitiven Operationen" (gemäß der Musterbeschreibung) in 12 verschieben Unterklassen. Dies wird möglicherweise die Wartbarkeit und Testbarkeit verbessern und tatsächlich dieselbe Leistung wie der aktuelle Code haben, da die JVM nach einer Weile die Methodenaufrufe für die primitiven Operationen einbinden kann. Dies muss natürlich in einem Leistungstest überprüft werden.


0

Sie können das Problem spezialisierter Methoden mithilfe der Scala-Sprache lösen.

Scala kann Inline-Methoden verwenden. Dies (in Kombination mit der einfachen Verwendung von Funktionen höherer Ordnung) ermöglicht die kostenlose Vermeidung von Code-Duplikaten - dies scheint das Hauptproblem zu sein, das in Ihrer Antwort erwähnt wurde.

Scala verfügt aber auch über syntaktische Makros, die es ermöglichen, beim Kompilieren auf typsichere Weise eine Menge Dinge mit Code zu tun.

Das häufig auftretende Problem des Boxens von primitiven Typen bei der Verwendung in Generika kann auch in Scala gelöst werden: Es kann eine generische Spezialisierung für Primitive durchführen, um das automatische Boxen durch Verwendung von @specializedAnnotationen zu vermeiden - dieses Element ist in die Sprache selbst integriert. Im Grunde schreiben Sie eine allgemeine Methode in Scala, die mit der Geschwindigkeit spezialisierter Methoden ausgeführt wird. Und wenn Sie auch schnelle generische Arithmetik benötigen, können Sie ganz einfach Typeclass "pattern" verwenden, um die Operationen und Werte für verschiedene numerische Typen einzufügen.

Scala ist auch auf viele andere Arten fantastisch. Und Sie müssen nicht den gesamten Code in Scala umschreiben, da die Interoperabilität von Scala <-> Java hervorragend ist. Stellen Sie einfach sicher, dass Sie SBT (Scala Build Tool) zum Erstellen des Projekts verwenden.


-1

Wenn die betreffende Funktion groß ist, kann es funktionieren, die "mode / paradigm" -Bits in eine Schnittstelle zu verwandeln und dann ein Objekt zu übergeben, das diese Schnittstelle als Parameter für die Funktion implementiert. GoF nennt dies das "Strategie" -Muster, iirc. Wenn die Funktion klein ist, kann der Mehraufwand erheblich sein. Hat jemand schon das Profilieren erwähnt? Mehr Leute sollten das Profilieren erwähnen.


Dies liest sich eher wie ein Kommentar als eine Antwort
Mücke

-2

Wie alt ist das Programm? Höchstwahrscheinlich würde eine neuere Hardware den Engpass beseitigen und Sie könnten zu einer wartbareren Version wechseln. Es muss jedoch einen Grund für die Wartung geben. Wenn Sie also nicht die Codebasis verbessern möchten, lassen Sie es so, wie es funktioniert.


1
dies scheint nur wiederholen Sie die Punkte gemacht und erklärt in einer vor Antwort vor wenigen Stunden geschrieben
gnat

1
Die einzige Möglichkeit, festzustellen, wo ein Engpass tatsächlich liegt, besteht darin, ihn zu profilieren - wie in der anderen Antwort erwähnt. Ohne diese Schlüsselinformationen ist jede vorgeschlagene Korrektur der Leistung reine Spekulation.
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.