W dość dużym projekcie znajduje się plik źródłowy z kilkoma funkcjami niezwykle wrażliwymi na wydajność (nazywanymi milionami razy na sekundę). W rzeczywistości poprzedni opiekun postanowił napisać 12 kopii funkcji różniących się bardzo nieznacznie, aby zaoszczędzić czas, który zostałby poświęcony na sprawdzenie warunków w jednej funkcji.
Niestety oznacza to, że kod jest PITA do utrzymania. Chciałbym usunąć cały zduplikowany kod i napisać tylko jeden szablon. Jednak język Java nie obsługuje szablonów i nie jestem pewien, czy nadają się do tego generyczne produkty.
Mój obecny plan polega na tym, aby zamiast tego napisać plik, który generuje 12 kopii funkcji (praktycznie ekspander szablonów jednorazowego użytku). Oczywiście podałbym obszerne wyjaśnienie, dlaczego plik musi być generowany programowo.
Obawiam się, że doprowadziłoby to do zamieszania przyszłych opiekunów i być może wprowadziłoby nieprzyjemne błędy, jeśli zapomną zregenerować plik po modyfikacji, lub (co gorsza), jeśli zmodyfikują zamiast tego plik wygenerowany programowo. Niestety, poza przepisaniem całego tekstu w C ++, nie widzę sposobu, aby to naprawić.
Czy zalety tego podejścia przeważają nad wadami? Czy zamiast tego powinienem:
- Wykorzystaj wydajność i skorzystaj z jednej, łatwej do utrzymania funkcji.
- Dodaj wyjaśnienia, dlaczego funkcja musi być powielona 12 razy, i uprzejmie weź na siebie obciążenie związane z utrzymaniem.
- Spróbuj użyć generycznych jako szablonów (prawdopodobnie nie działają w ten sposób).
- Krzycz na starego opiekuna, aby uczynić kod tak zależnym od wydajności od jednej funkcji.
- Inna metoda utrzymania wydajności i łatwości konserwacji?
PS Z powodu złej konstrukcji projektu profilowanie funkcji jest dość trudne ... jednak były opiekun przekonał mnie, że hit wydajności jest niedopuszczalny. Zakładam, że przez to rozumie on ponad 5%, choć z mojej strony jest to całkowite przypuszczenie.
Może powinienem trochę rozwinąć. 12 kopii wykonuje bardzo podobne zadanie, ale ma niewielkie różnice. Różnice występują w różnych miejscach funkcji, więc niestety istnieje wiele, wiele instrukcji warunkowych. Istnieje skutecznie 6 „trybów” działania i 2 „paradygmaty” działania (słowa wymyślone przeze mnie). Aby użyć tej funkcji, określa się „tryb” i „paradygmat” działania. To nigdy nie jest dynamiczne; każdy fragment kodu używa dokładnie jednego trybu i paradygmatu. Wszystkie 12 par tryb-paradygmat są używane gdzieś w aplikacji. Funkcje są odpowiednio nazwane func1 do func12, przy czym liczby parzyste reprezentują drugi paradygmat, a liczby nieparzyste reprezentują pierwszy paradygmat.
Zdaję sobie sprawę, że jest to chyba najgorszy projekt w historii, jeśli celem jest łatwość konserwacji. Ale wydaje się, że jest „wystarczająco szybki”, a ten kod przez jakiś czas nie wymagał żadnych zmian ... Warto również zauważyć, że oryginalna funkcja nie została usunięta (chociaż, o ile wiem, jest to martwy kod) , więc refaktoryzacja byłaby prosta.
źródło
Makefile
” (lub innego używanego systemu) i usuń go zaraz po zakończeniu kompilacji . W ten sposób po prostu nie mają możliwości zmodyfikowania niewłaściwego pliku źródłowego.Odpowiedzi:
Jest to bardzo zła sytuacja, trzeba byłaby to jak najszybciej - to dług techniczny to najgorsze - nie wiem nawet, jak ważny kod naprawdę jest - tylko spekulować, że to ważne.
Co do rozwiązań JAK NAJSZYBCIEJ:
Coś, co można zrobić, to dodać niestandardowy krok kompilacji. Jeśli używasz Mavena, który jest w rzeczywistości dość prosty, inne automatyczne systemy kompilacji również mogą sobie z tym poradzić. Napisz plik z innym rozszerzeniem niż .java i dodaj niestandardowy krok, który przeszuka źródło w poszukiwaniu takich plików i ponownie wygeneruje rzeczywistą wersję .java. Możesz także dodać ogromne zastrzeżenie do automatycznie generowanego pliku, wyjaśniające, że nie należy go modyfikować.
Plusy a używanie raz wygenerowanego pliku: Twoi programiści nie uzyskają zmian w .java. Jeśli faktycznie uruchomią kod na swoim komputerze przed zatwierdzeniem, stwierdzą, że ich zmiany nie mają wpływu (hah). A potem może przeczytają zastrzeżenie. Masz absolutną rację, nie ufając kolegom z drużyny i przyszłemu sobie, pamiętając, że ten konkretny plik musi zostać zmieniony w inny sposób. Pozwala również na automatyczne testowanie tak dobrze, jak JUnit skompiluje Twój program przed uruchomieniem testów (i również ponownie wygeneruje plik)
EDYTOWAĆ
Sądząc po komentarzach, odpowiedź wyszła tak, jakby to był sposób na to, aby działało to w nieskończoność i być może OK będzie można wdrożyć w innych częściach projektu o kluczowym znaczeniu dla wydajności.
Mówiąc prosto: nie jest.
Dodatkowy ciężar tworzenia własnego mini-języka, pisania dla niego generatora kodu i utrzymywania go, nie wspominając o uczeniu go dla przyszłych opiekunów, jest piekielny na dłuższą metę. Powyższe pozwala jedynie na bezpieczniejszy sposób rozwiązania problemu podczas pracy nad długoterminowym rozwiązaniem . To, co to zajmie, będzie nade mną.
źródło
ant clean
cokolwiek innego. Nie musisz umieszczać zrzeczenia się odpowiedzialności w pliku, którego nawet tam nie ma!Czy konserwacja jest prawdziwa, czy może ci to przeszkadza? Jeśli ci to przeszkadza, zostaw to w spokoju.
Czy problem z wydajnością jest prawdziwy, czy może tylko poprzedni programista tak sądził ? Problemy z wydajnością, częściej niż nie, nie występują tam, gdzie są uważane, nawet jeśli używany jest profiler. (Używam tej techniki, aby niezawodnie je znaleźć).
Istnieje więc możliwość, że masz brzydkie rozwiązanie problemu, ale może to być brzydkie rozwiązanie prawdziwego problemu. W razie wątpliwości zostaw to w spokoju.
źródło
Naprawdę upewnij się, że w rzeczywistych warunkach produkcyjnych (realistyczne zużycie pamięci, które realistycznie wyzwalają czyszczenie pamięci itp.), Wszystkie te oddzielne metody naprawdę mają wpływ na wydajność (w przeciwieństwie do posiadania tylko jednej metody). To zajmie ci kilka dni, ale może zaoszczędzić tygodnie, upraszczając kod, z którym teraz pracujesz.
Ponadto, jeśli zrobić odkryć, że trzeba wszystkie funkcje, można użyć Javassist do generowania kodu programowo. Jak zauważyli inni, możesz zautomatyzować to za pomocą Maven .
źródło
Dlaczego nie uwzględnić jakiegoś generatora szablonów \ generowania kodu dla tego systemu? Niestandardowy dodatkowy etap kompilacji, który wykonuje i emituje dodatkowe pliki źródłowe Java przed skompilowaniem pozostałej części kodu. W ten sposób często działa włączanie klientów WWW z plików wsdl i xsd.
Oczywiście będziesz mieć obsługę tego generatora kodu / preprocesora, ale nie będziesz musiał się martwić o obsługę zduplikowanego kodu podstawowego.
Ponieważ emitowany jest kod Java w czasie kompilacji, za dodatkowy kod nie trzeba płacić za wydajność. Ale zyskujesz na uproszczeniu konserwacji dzięki szablonowi zamiast całego zduplikowanego kodu.
Generyczne Java nie dają żadnej korzyści w zakresie wydajności ze względu na wymazywanie tekstu w języku, zamiast niego zastosowano proste rzutowanie.
Z mojego zrozumienia szablonów C ++ są one zestawiane w kilka funkcji dla każdego wywołania szablonu. Skończysz ze zduplikowanym kodem czasowym w połowie kompilacji dla każdego typu przechowywanego na przykład w standardzie std :: vector.
źródło
Żadna rozsądna metoda Java nie może być wystarczająco długa, aby mieć 12 wariantów ... a JITC nie znosi długich metod - po prostu odmawia ich właściwej optymalizacji. Widziałem współczynnik przyspieszenia równy dwa, po prostu dzieląc metodę na dwie krótsze. Może to jest właściwa droga.
OTOH posiadające wiele kopii może mieć sens, nawet jeśli były identyczne. Gdy każdy z nich przyzwyczaja się w różnych miejscach, są one optymalizowane pod kątem różnych przypadków (JITC profiluje je, a następnie umieszcza rzadkie przypadki na wyjątkowej ścieżce.
Powiedziałbym, że generowanie kodu nie jest niczym wielkim, zakładając, że jest dobry powód. Narzut jest raczej niski, a odpowiednio nazwane pliki prowadzą bezpośrednio do ich źródła. Jakiś czas temu, kiedy generowałem kod źródłowy, wstawiałem
// DO NOT EDIT
każdą linię ... Myślę, że to wystarczy.źródło
Nie ma absolutnie nic złego we wspomnianym „problemie”. Z tego, co wiem, jest to dokładnie rodzaj projektu i podejście zastosowane przez serwer DB w celu uzyskania dobrej wydajności.
Mają wiele specjalnych metod, aby upewnić się, że mogą zmaksymalizować wydajność dla wszystkich rodzajów operacji: łączyć, wybierać, agregować itp., Gdy mają zastosowanie określone warunki.
Krótko mówiąc, generowanie kodu takiego jak ten, który uważasz za zły pomysł. Być może powinieneś spojrzeć na ten schemat, aby zobaczyć, jak DB rozwiązuje problem podobny do twojego:
źródło
Możesz sprawdzić, czy nadal możesz używać sensownych abstrakcji i użyć np. Wzorca metody szablonu, aby napisać zrozumiały kod dla wspólnej funkcjonalności i przenieść różnice między metodami do „operacji prymitywnych” (zgodnie z opisem wzoru) w 12 podklasy. To znacznie poprawi łatwość konserwacji i testowania, i może faktycznie mieć taką samą wydajność jak bieżący kod, ponieważ JVM może po pewnym czasie wstawić wywołania metod do operacji pierwotnych. Oczywiście musisz to sprawdzić w teście wydajności.
źródło
Możesz rozwiązać problem metod specjalistycznych za pomocą języka Scala.
Scala może wstawiać metody, które (w połączeniu z łatwym użyciem funkcji wyższego rzędu) pozwalają uniknąć bezpłatnego powielania kodu - wygląda to na główny problem wymieniony w odpowiedzi.
Ale Scala ma również makra składniowe, które umożliwiają wykonywanie wielu rzeczy przy użyciu kodu w czasie kompilacji w sposób bezpieczny dla typu.
I powszechny problem boksowania typów prymitywów, gdy są używane w rodzajach, jest również możliwy do rozwiązania w Scali: może on robić ogólną specjalizację dla elementów podstawowych, aby uniknąć boksu automatycznie za pomocą
@specialized
adnotacji - ta rzecz jest wbudowana w sam język. Zasadniczo w Scali napiszesz jedną ogólną metodę, która będzie działać z szybkością metod specjalistycznych. A jeśli potrzebujesz również szybkiej arytmetyki ogólnej, można to łatwo zrobić za pomocą „wzorca” Typeclass do wstrzykiwania operacji i wartości dla różnych typów liczbowych.Ponadto Scala jest niesamowity na wiele innych sposobów. I nie musisz przepisywać całego kodu do Scali, ponieważ interoperacyjność Scala <-> Java jest doskonała. Upewnij się, że używasz SBT (narzędzie do budowania scala) do budowania projektu.
źródło
Jeśli dana funkcja jest duża, przekształcenie bitów „tryb / paradygmat” w interfejs, a następnie przekazanie obiektu, który implementuje ten interfejs jako parametr do funkcji, może działać. GoF nazywa to wzorem „strategii”, iirc. Jeśli funkcja jest niewielka, zwiększony narzut może być znaczący ... czy ktoś wspomniał już o profilowaniu? ... więcej osób powinno wspomnieć o profilowaniu.
źródło
Ile lat ma ten program? Najprawdopodobniej nowszy sprzęt usunąłby wąskie gardło i można by przełączyć na łatwiejszą do utrzymania wersję. Ale musi istnieć powód do konserwacji, więc chyba że Twoim zadaniem jest poprawienie bazy kodu, pozostaw to tak, jak działa.
źródło