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.
quelle
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.Antworten:
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.
quelle
ant clean
oder was auch immer Ihre Entsprechung ist. Sie müssen keinen Haftungsausschluss in eine Datei einfügen, die noch nicht einmal vorhanden ist!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.
quelle
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 .
quelle
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.
quelle
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 EDIT
jede Zeile eingefügt ... Ich denke, das ist sicher genug.quelle
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:
quelle
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.
quelle
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
@specialized
Annotationen 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.
quelle
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.
quelle
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.
quelle