Odpowiadając na to pytanie , zacząłem się zastanawiać, dlaczego tak wielu programistów uważa, że dobry projekt nie powinien uwzględniać wydajności, ponieważ wpłynie to na czytelność i / lub łatwość konserwacji.
Uważam, że dobry projekt uwzględnia również wydajność w momencie pisania, i że dobry programista z dobrym projektem może napisać wydajny program bez negatywnego wpływu na czytelność lub łatwość konserwacji.
Chociaż zdaję sobie sprawę z tego, że zdarzają się przypadki ekstremalne, dlaczego wielu programistów twierdzi, że efektywny program / projekt spowoduje słabą czytelność i / lub słabą konserwację, a w związku z tym wydajność nie powinna być rozważana przy projektowaniu?
Odpowiedzi:
Myślę, że takie poglądy są zwykle reakcjami na próby przedwczesnej (mikro) optymalizacji , która wciąż jest powszechna i zwykle wyrządza więcej szkody niż pożytku. Kiedy ktoś próbuje przeciwstawić się takim poglądom, łatwo wpaść w - lub przynajmniej wyglądać - drugą skrajność.
Prawdą jest jednak, że w związku z ogromnym rozwojem zasobów sprzętowych w ostatnich dziesięcioleciach, w przypadku większości pisanych obecnie programów, wydajność przestała być głównym czynnikiem ograniczającym. Oczywiście należy wziąć pod uwagę oczekiwaną i możliwą do osiągnięcia wydajność na etapie projektowania, aby zidentyfikować przypadki, w których wydajność może (przyjść) poważnym problemem . A potem naprawdę ważne jest, aby projektować od samego początku. Jednak ogólna prostota, czytelność i łatwość konserwacji są jeszcze ważniejsze . Jak zauważyli inni, kod zoptymalizowany pod kątem wydajności jest bardziej złożony, trudniejszy do odczytania i utrzymania oraz bardziej podatny na błędy niż najprostsze działające rozwiązanie. Dlatego każdy wysiłek włożony w optymalizację musi zostać udowodniony - a nie tylko uwierzony- przynieść rzeczywiste korzyści, przy jak najmniejszym ograniczeniu długoterminowej możliwości utrzymania programu. Tak więc dobry projekt izoluje skomplikowane części wymagające dużej wydajności od reszty kodu , który jest tak prosty i czysty, jak to możliwe.
źródło
W odpowiedzi na twoje pytanie ze strony programisty, który pracuje nad kodem o wysokiej wydajności, przy projektowaniu należy wziąć pod uwagę kilka kwestii.
Zrób to dobrze, zrób to pięknie, zrób to szybko. W tej kolejności.
źródło
contains
której często będziesz dzwonić , użyj, aHashSet
nieArrayList
. Wydajność może nie mieć znaczenia, ale nie ma powodu, aby tego nie robić. Wykorzystaj zgodność między dobrym projektem a wydajnością - jeśli przetwarzasz pewną kolekcję, spróbuj zrobić wszystko w jednym przejściu, które prawdopodobnie będzie zarówno bardziej czytelne, jak i szybsze (prawdopodobnie).Jeśli mogę założyć „pożyczyć” ładny diagram @ greengit i zrobić mały dodatek:
Wszyscy zostali „nauczeni”, że istnieją krzywe kompromisów. Ponadto wszyscy założyliśmy, że jesteśmy tak optymalnymi programistami, że każdy program, który piszemy, jest tak napięty, że jest na dobrej drodze . Jeśli program jest na krzywej, każda poprawa w jednym wymiarze koniecznie wiąże się z kosztem w drugim wymiarze.
Z mojego doświadczenia wynika, że programy zbliżają się do dowolnej krzywej tylko wtedy, gdy są dostrajane, modyfikowane, młotkowane, woskowane i ogólnie zamieniane w „golfa kodowego”. Większość programów ma dużo miejsca na ulepszenia we wszystkich wymiarach. Oto co mam na myśli.
źródło
Właśnie dlatego, że wysoce wydajne komponenty oprogramowania są na ogół o rząd wielkości bardziej złożone niż inne komponenty oprogramowania (wszystkie inne rzeczy są równe).
Nawet wtedy nie jest to tak jednoznaczne, jeśli wskaźniki wydajności są niezwykle ważnym wymogiem, konieczne jest, aby projekt miał złożoność, aby sprostać takim wymaganiom. Niebezpieczeństwo polega na tym, że deweloper marnuje sprint na stosunkowo prostą funkcję, próbując wycisnąć kilka dodatkowych milisekund z jego komponentu.
Niezależnie od tego złożoność projektu ma bezpośredni związek ze zdolnością programisty do szybkiego uczenia się i zapoznania się z takim projektem, a dalsze modyfikacje funkcjonalności złożonego komponentu mogą powodować błędy, które mogą nie zostać wykryte przez testy jednostkowe. Złożone projekty mają wiele innych aspektów i możliwych przypadków testowych, aby rozważyć uczynienie celu 100% pokrycia testem jednostkowym jeszcze bardziej marzeniem.
Biorąc to pod uwagę, należy zauważyć, że słabo działający komponent oprogramowania może działać słabo tylko dlatego, że został niemądrze napisany i niepotrzebnie skomplikowany w oparciu o ignorancję oryginalnego autora (wykonanie 8 wywołań bazy danych w celu zbudowania pojedynczego elementu, gdy tylko jeden zrobiłby to , całkowicie niepotrzebny kod, który skutkuje pojedynczą ścieżką kodu, niezależnie od tego, itp.). Te przypadki są bardziej związane z poprawą jakości kodu i wzrostem wydajności zachodzącym w wyniku refaktora, a niekoniecznie zamierzonej konsekwencji.
Zakładając dobrze zaprojektowany komponent, zawsze będzie on jednak mniej skomplikowany niż podobnie dobrze zaprojektowany komponent dostrojony pod kątem wydajności (wszystkie inne rzeczy będą takie same).
źródło
Nie chodzi o to, że te rzeczy nie mogą współistnieć. Problem polega na tym, że kod każdego jest powolny, nieczytelny i niemożliwy do utrzymania przy pierwszej iteracji. Resztę czasu poświęca się na doskonalenie tego, co najważniejsze. Jeśli to jest wydajność, wybierz ją. Nie pisz złośliwie okropnego kodu, ale jeśli po prostu musi być X szybki, to spraw, by był X szybki. Uważam, że wydajność i czystość są w zasadzie nieskorelowane. Kod wykonawczy nie powoduje brzydkiego kodu. Jeśli jednak spędzasz czas na dostrajaniu każdego kawałka kodu, aby być szybkim, zgadnij, czego nie poświęciłeś? Twój kod jest czysty i łatwy w utrzymaniu.
źródło
Jak widzisz...
Wydajność i czytelność są jednak w niewielkim stopniu powiązane - w większości przypadków nie ma naprawdę dużych zachęt, które preferowałyby te pierwsze. Mówię tu o językach wysokiego poziomu.
źródło
Moim zdaniem wydajność powinna być brana pod uwagę, gdy jest to rzeczywisty problem (lub np. Wymóg). Nieprzestrzeganie tego prowadzi do mikrooptymalizacji, co może prowadzić do bardziej zaciemnionego kodu, aby zaoszczędzić kilka mikrosekund tu i tam, co z kolei prowadzi do mniej konserwowalnego i mniej czytelnego kodu. Zamiast tego należy w razie potrzeby skoncentrować się na prawdziwych wąskich gardłach systemu i położyć nacisk na wydajność.
źródło
Chodzi o to, że czytelność nie powinna zawsze przewyższać wydajności. Jeśli od samego początku wiesz, że Twój algorytm musi być bardzo wydajny, będzie to jeden z czynników, które wykorzystasz do jego opracowania.
Chodzi o to, że większość przypadków użycia nie wymaga oślepiającego szybkiego kodu. W wielu przypadkach interakcja IO lub użytkownika powoduje znacznie większe opóźnienie niż wykonanie algorytmu. Chodzi o to, że nie powinieneś starać się uczynić czegoś bardziej wydajnym, jeśli nie wiesz, że to szyjka butelki.
Optymalizacja kodu pod kątem wydajności często czyni go bardziej skomplikowanym, ponieważ zazwyczaj wymaga robienia rzeczy w sprytny sposób, a nie w najbardziej intuicyjny sposób. Bardziej skomplikowany kod jest trudniejszy w utrzymaniu i trudniejszy do pobrania dla innych programistów (oba są kosztami, które należy wziąć pod uwagę). Jednocześnie kompilatory są bardzo dobre w optymalizacji typowych przypadków. Możliwe, że próba ulepszenia zwykłego przypadku oznacza, że kompilator nie rozpoznaje już wzorca, a zatem nie może pomóc w szybkim utworzeniu kodu. Należy zauważyć, że nie oznacza to pisania, co chcesz, bez względu na wydajność. Nie powinieneś robić niczego, co jest wyraźnie nieefektywne.
Chodzi o to, aby nie martwić się drobiazgami, które mogłyby ulepszyć. Użyj profilera i przekonaj się, że 1) masz teraz problem i 2) zmieniłeś go na ulepszenie.
źródło
Myślę, że większość programistów odczuwa to przeczucie po prostu dlatego, że przez większość czasu kod wydajności jest kodem opartym na dużo większej ilości informacji (o kontekście, wiedzy o sprzęcie, globalnej architekturze) niż jakikolwiek inny kod w aplikacjach. Większość kodu wyraża tylko niektóre rozwiązania określonych problemów, które są enkapsulowane w niektórych abstrakcjach w sposób modułowy (jak funkcje), a to oznacza ograniczanie znajomości kontekstu tylko do tego, co wchodzi w tę enkapsulację (jak parametry funkcji).
Kiedy piszesz w celu uzyskania wysokiej wydajności, po naprawieniu jakiejkolwiek optymalizacji algorytmicznej, wchodzisz w szczegóły, które wymagają znacznie większej wiedzy na temat kontekstu. To naturalnie może przytłoczyć każdego programistę, który nie czuje się wystarczająco skoncentrowany na tym zadaniu.
źródło
Ponieważ koszt globalnego ocieplenia (z tych dodatkowych cykli procesora skalowanych przez setki milionów komputerów PC oraz ogromne zaplecze centrum danych) i przeciętnej żywotności baterii (na urządzeniach mobilnych użytkownika), wymaganej do uruchomienia ich źle zoptymalizowanego kodu, rzadko pojawia się na większości wydajność programisty lub recenzje.
Jest to ekonomiczny negatywny efekt zewnętrzny, podobny do formy ignorowanego zanieczyszczenia. Tak więc stosunek kosztów do korzyści myślenia o wydajności w ogóle jest mentalnie wypaczony z rzeczywistości.
Projektanci sprzętu ciężko pracują, dodając funkcje oszczędzania energii i skalowania zegara do najnowszych procesorów. Programiści powinni pozwolić sprzętowi częściej korzystać z tych możliwości, nie rozgryzając każdego dostępnego cyklu zegara procesora.
DODANO: W czasach starożytnych koszt jednego komputera wynosił miliony, więc optymalizacja czasu procesora była bardzo ważna. Wtedy koszt opracowania i utrzymania kodu stał się większy niż koszt komputerów, więc optymalizacja wypadła zdecydowanie nie na korzyść w porównaniu z wydajnością programisty. Teraz jednak inny koszt staje się większy niż koszt komputerów, koszt zasilania i chłodzenia wszystkich tych centrów danych jest teraz większy niż koszt wszystkich procesorów w środku.
źródło
Myślę, że trudno jest osiągnąć wszystkie trzy. Dwa, jak sądzę, mogą być wykonalne. Na przykład myślę, że w niektórych przypadkach możliwe jest osiągnięcie wydajności i czytelności, ale łatwość konserwacji może być trudna w przypadku mikrodostrojenia kodu. Najbardziej wydajnemu kodowi na tej planecie na ogół brakuje zarówno łatwości konserwacji, jak i czytelności, co prawdopodobnie jest oczywiste dla większości, chyba że jesteś osobą, która rozumie ręcznie wektoryzowany SoA, wielowątkowy kod SIMD, który Intel zapisuje z wbudowanym złożeniem lub najbardziej wycinany algorytmy krawędziowe stosowane w branży z 40-stronicowymi artykułami matematycznymi opublikowanymi zaledwie 2 miesiące temu i kodem o wartości 12 bibliotek dla jednej niezwykle złożonej struktury danych.
Mikroefektywność
Sugeruję, że może być sprzeczne z popularną opinią, że najmądrzejszy kod algorytmiczny jest często trudniejszy w utrzymaniu niż najbardziej precyzyjny algorytm. Pomysł, że ulepszenia skalowalności przynoszą większe zyski w porównaniu z mikrodostrojonym kodem (np. Wzorce dostępu przyjazne dla pamięci podręcznej, wielowątkowość, SIMD itp.) Jest czymś, co rzuciłbym wyzwanie, przynajmniej pracując w branży wypełnionej niezwykle złożonym struktury danych i algorytmy (branża efektów wizualnych), szczególnie w obszarach takich jak przetwarzanie siatki, ponieważ huk może być duży, ale złotówka jest niezwykle droga, gdy wprowadzasz nowe algorytmy i struktury danych, o których nikt wcześniej nie słyszał, ponieważ są marką Nowy. Ponadto ja
Pomysł, że optymalizacje algorytmiczne zawsze przebijają, powiedzmy, optymalizacje związane z wzorcami dostępu do pamięci, jest zawsze czymś, z czym się nie zgadzam. Oczywiście, jeśli używasz sortowania bąbelkowego, żadna ilość mikrooptymalizacji nie może ci w tym pomóc ... ale z uzasadnionego powodu nie sądzę, że zawsze jest to tak jednoznaczne. Prawdopodobnie optymalizacje algorytmiczne są trudniejsze do utrzymania niż mikrooptymalizacje. Łatwiej byłoby mi powiedzieć, powiedzmy, Embree Intela, który pobiera klasyczny i prosty algorytm BVH i po prostu mikro-tuninguje bzdury z niego niż kod OpenVDB Dreamwork dla najnowszych metod algorytmicznego przyspieszania symulacji płynów. Przynajmniej w mojej branży chciałbym zobaczyć więcej osób zaznajomionych z mikrooptymalizacją architektury komputerów, tak jak Intel, gdy wkroczyli na scenę, w przeciwieństwie do wymyślania tysięcy nowych algorytmów i struktur danych. Dzięki skutecznym mikrooptymalizacjom ludzie mogą znaleźć coraz mniej powodów do wymyślania nowych algorytmów.
Pracowałem wcześniej w bazie kodu, gdzie prawie każda operacja użytkownika miała swoją unikalną strukturę danych i algorytm (dodając setki egzotycznych struktur danych). I większość z nich miała bardzo wypaczoną charakterystykę działania, ponieważ ma bardzo wąskie zastosowanie. Byłoby o wiele łatwiej, gdyby system mógł obracać się wokół kilkudziesięciu bardziej rozpowszechnionych struktur danych, i myślę, że mogłoby tak być, gdyby były one znacznie lepiej zoptymalizowane pod kątem mikro. Wspominam o tym przypadku, ponieważ mikrooptymalizacja może potencjalnie znacznie poprawić łatwość konserwacji w takim przypadku, jeśli oznacza to różnicę między setkami mikropesymizowanych struktur danych, których nie można nawet bezpiecznie używać do celów ścisłego odczytu, które wiążą się z brakami pamięci podręcznej i prawda vs.
Języki funkcjonalne
Tymczasem niektóre z najbardziej konserwowalnych kodów, jakie kiedykolwiek spotkałem, były dość wydajne, ale bardzo trudne do odczytania, ponieważ zostały napisane w językach funkcjonalnych. Ogólnie, moim zdaniem, czytelność i łatwość konserwacji są sprzecznymi pomysłami.
Naprawdę trudno jest uczynić kod czytelnym, łatwym w utrzymaniu i wydajnym jednocześnie. Zazwyczaj trzeba trochę pójść na kompromis w jednym z tych trzech, jeśli nie w dwóch, na przykład pogarszając czytelność pod kątem łatwości konserwacji lub podważając łatwość konserwacji pod względem wydajności. Zazwyczaj jest to łatwość utrzymania, gdy szukasz wielu pozostałych dwóch.
Czytelność a łatwość konserwacji
Jak już powiedziano, uważam, że czytelność i łatwość konserwacji nie są harmonijnymi koncepcjami. W końcu najbardziej czytelny kod dla większości z nas śmiertelników bardzo intuicyjnie odwzorowuje ludzkie wzorce myślowe, a wzorce ludzkich myśli są z natury podatne na błędy: „ Jeśli tak się stanie, zrób to. Jeśli tak się stanie, zrób to. W przeciwnym razie zrób to. Ups , Zapomniałem czegoś! Jeśli te systemy współdziałają ze sobą, powinno to się zdarzyć, aby ten system mógł to zrobić ... och, czekaj, co z tym systemem, gdy to zdarzenie zostanie wywołane?„Zapomniałem dokładnego cytatu, ale ktoś kiedyś powiedział, że gdyby Rzym zbudowano jak oprogramowanie, wystarczyłoby ptaka lądującego na ścianie, aby go przewrócić. Tak jest w przypadku większości programów. Jest to bardziej delikatne, niż nam się często wydaje Pomyśl o tym. Kilka wierszy pozornie nieszkodliwego kodu tu i tam może zatrzymać go do tego stopnia, że zmusimy nas do ponownego przemyślenia całego projektu, a języki wysokiego poziomu, które mają być jak najbardziej czytelne, nie są wyjątkiem od takich błędów w projektowaniu przez ludzi .
Czyste języki funkcjonalne są prawie tak niewrażliwe na to, jak to tylko możliwe (nawet bliskie niewrażliwości, ale stosunkowo dużo bliżej niż większość). A częściowo dlatego, że nie odwzorowują intuicyjnie ludzkich myśli. Nie są czytelne. Wymuszają na nas wzorce myślenia, które powodują, że musimy rozwiązywać problemy w jak najmniejszej liczbie przypadków szczególnych, wykorzystując minimalną możliwą wiedzę i nie powodując żadnych skutków ubocznych. Są niezwykle ortogonalne, pozwalają często zmieniać i zmieniać kod bez niespodzianek tak epickich, że musimy przemyśleć projekt na desce kreślarskiej, nawet do tego stopnia, że zmieniamy zdanie na temat ogólnego projektu, bez przepisywania wszystkiego. Wydaje się, że nie jest łatwiejsze do utrzymania niż to ... ale kod jest nadal bardzo trudny do odczytania,
źródło
Jednym z problemów jest to, że skończony czas programisty oznacza, że wszystko, co chcesz zoptymalizować, zabiera poświęcanie czasu na inne kwestie.
Jest dość dobry eksperyment na tym, o którym mowa w Meyer Code Complete. Poproszono różne grupy programistów o optymalizację pod kątem szybkości, wykorzystania pamięci, czytelności, odporności i tak dalej. Stwierdzono, że ich projekty uzyskały wysokie wyniki we wszystkich aspektach optymalizacji, ale niższe we wszystkich innych aspektach.
źródło
Ponieważ doświadczeni programiści nauczyli się, że to prawda.
Pracowaliśmy z kodem, który jest oszczędny i wredny i nie ma problemów z wydajnością.
Pracowaliśmy nad wieloma kodami, które w celu rozwiązania problemów z wydajnością są BARDZO złożone.
Jednym z bezpośrednich przykładów, który przychodzi mi na myśl, jest to, że mój ostatni projekt obejmował 8192 ręcznie dzielonych tabel SQL. Było to potrzebne z powodu problemów z wydajnością. Konfiguracja wyboru z 1 tabeli jest o wiele prostsza niż wybranie i utrzymanie 8192 odłamków.
źródło
Istnieją również słynne fragmenty wysoce zoptymalizowanego kodu, który wygina mózgi większości ludzi, które wspierają przypadek, w którym wysoce zoptymalizowany kod jest trudny do odczytania i zrozumienia.
Oto najbardziej znany, jak sądzę. Zaczerpnięte z Quake III Arena i przypisane Johnowi Carmakowi, chociaż myślę, że było kilka iteracji tej funkcji i nie została przez niego pierwotnie stworzona ( czy Wikipedia nie jest świetna? ).
źródło