Czy można zastąpić zoptymalizowany kod czytelnym kodem?


78

Czasami napotykasz sytuację, w której musisz rozszerzyć / ulepszyć istniejący kod. Widzisz, że stary kod jest bardzo ubogi, ale również trudny do rozszerzenia i wymaga czasu.

Czy warto zastąpić go nowoczesnym kodem?

Jakiś czas temu podobało mi się podejście lean, ale teraz wydaje mi się, że lepiej poświęcić wiele optymalizacji na rzecz wyższych abstrakcji, lepszych interfejsów i bardziej czytelnego, rozszerzalnego kodu.

Wydaje się, że kompilatory struct abc = {}stają się coraz lepsze, więc rzeczy takie jak po cichu zamieniane są w memsets, shared_ptrs generują prawie ten sam kod, co kręcenie surowego wskaźnika, szablony działają bardzo dobrze, ponieważ wytwarzają super ubogi kod i tak dalej.

Ale nadal czasami widzisz tablice oparte na stosie i stare funkcje C z pewną niejasną logiką, i zwykle nie są one na krytycznej ścieżce.

Czy warto zmienić taki kod, jeśli musisz dotknąć jego małego fragmentu w obu kierunkach?


20
Czytelność i optymalizacje przez większość czasu nie są przeciwne.
deadalnix

23
Czy czytelność może się poprawić dzięki niektórym komentarzom?
YetAnotherUser

17
Niepokojące jest to, że OOP-ification jest uważane za „nowoczesny kod”
James

7
jak filozofia slackware: jeśli nie jest zepsuta, nie naprawiaj tego, masz mniej, bardzo dobry powód, aby to zrobić
osdamv

5
Czy poprzez zoptymalizowany kod masz na myśli rzeczywisty zoptymalizowany kod, czy tak zwany zoptymalizowany kod?
dan04

Odpowiedzi:


115

Gdzie?

  • Na stronie głównej witryny w skali Google jest to niedopuszczalne. Utrzymuj wszystko tak szybko, jak to możliwe.

  • W części aplikacji, z której korzysta jedna osoba raz w roku, jest całkowicie akceptowalne poświęcenie wydajności w celu uzyskania czytelności kodu.

Ogólnie, jakie są niefunkcjonalne wymagania dla części kodu, nad którym pracujesz? Jeśli akcja musi wykonać się poniżej 900 ms. w danym kontekście (maszyna, ładowanie itp.) 80% czasu, i faktycznie działa poniżej 200 ms. Na pewno w 100% przypadków kod będzie bardziej czytelny, nawet jeśli może to nieznacznie wpłynąć na wydajność. Z drugiej strony, jeśli ta sama akcja nigdy nie została wykonana w ciągu dziesięciu sekund, powinieneś raczej spróbować zobaczyć, co jest nie tak z wydajnością (lub przede wszystkim wymaganiem).

Ponadto, w jaki sposób poprawa czytelności obniży wydajność? Często programiści dostosowują zachowanie do przedwczesnej optymalizacji: boją się zwiększyć czytelność, wierząc, że drastycznie zniszczy to wydajność, podczas gdy bardziej czytelny kod wyda kilka mikrosekund więcej, wykonując tę ​​samą akcję.


47
+1! Jeśli nie masz liczb, zdobądź kilka liczb. Jeśli nie masz czasu na uzyskanie liczb, nie masz czasu, aby go zmienić.
Tacroy

49
Często programiści „optymalizują” w oparciu o mit i nieporozumienia, na przykład zakładając, że „C” jest szybszy niż „C ++” i unikając funkcji C ++ z ogólnego przekonania, że ​​rzeczy są szybsze bez liczb, aby je poprzeć. Przypomina mi programistę C, którego obserwowałem, który uważał, że gotojest szybszy niż w przypadku pętli. Jak na ironię, optymalizator radził sobie lepiej z pętlami, dzięki czemu kod był wolniejszy i trudniejszy do odczytania.
Steven Burnap,

6
Zamiast dodawać kolejną odpowiedź, dałem +1 tej odpowiedzi. Jeśli zrozumienie fragmentów kodu jest tak ważne, dobrze je skomentuj. Pracowałem w środowisku C / C ++ / Assembly ze starszym kodem sprzed dziesięciu lat z dziesiątkami współpracowników. Jeśli kod działa, zostaw go w spokoju i wróć do pracy.
Chris K

Dlatego zazwyczaj piszę tylko czytelny kod. Wydajność można osiągnąć, zmniejszając kilka gorących punktów.
Luca,

36

Zwykle nie .

Zmiana kodu może powodować nieprzewidziane problemy związane z podrzucaniem w innym miejscu w systemie (które czasem mogą pozostać niezauważone aż do dużo później w projekcie, jeśli nie masz solidnych testów jednostkowych i dymnych). Zwykle myślę „jeśli to nie jest zepsute, nie naprawiaj”.

Wyjątkiem od tej reguły jest implementacja nowej funkcji dotykającej tego kodu. Jeśli w tym momencie nie ma to sensu, a refaktoryzacja naprawdę musi się odbyć, to idź na to tak długo, jak długo czas refaktoryzacji (i wystarczające testy i bufor do poradzenia sobie z problemami domina) są uwzględnione w szacunkach.

Oczywiście profil, profil, profil , szczególnie jeśli jest to krytyczny obszar ścieżki.


2
Tak, ale zakładasz, że optymalizacja była potrzebna. Nie zawsze wiemy, czy tak było i prawdopodobnie chcemy to najpierw ustalić.
haylem

2
@haylem: Nie, zakładam, że kod działa jak jest. Zakładam również, że refaktoryzacja kodu niezmiennie spowoduje problemy z domieszką w innym miejscu w systemie (chyba że masz do czynienia z trywialną częścią kodu, która nie ma żadnych zewnętrznych zależności).
Demian Brecht

W tej odpowiedzi jest trochę prawdy, a jak na ironię dzieje się tak, ponieważ problemy z domieszką rzadko są dokumentowane, rozumiane, komunikowane, a nawet zwracane na nie uwagę deweloperów. Jeśli programiści lepiej rozumieją problemy, które zdarzały się w przeszłości, będą wiedzieli, co mierzyć , i będą bardziej pewni wprowadzania zmian w kodzie.
rwong

29

W skrócie: to zależy

  • Czy naprawdę będziesz potrzebować czy używać swojej refaktoryzowanej / ulepszonej wersji?

    • Czy istnieje konkretny zysk, natychmiastowy czy długoterminowy?
    • Czy to zysk tylko z uwagi na łatwość konserwacji, czy może naprawdę architektoniczny?
  • Czy to naprawdę wymaga optymalizacji?

    • Dlaczego?
    • Do jakiego celu docelowego musisz dążyć?

W szczegółach

Czy potrzebujesz oczyszczonych, błyszczących rzeczy?

Są tu rzeczy, na które trzeba uważać, i musisz określić granicę między tym, co jest realnym, wymiernym zyskiem, a tym, co jest twoją osobistą preferencją i potencjalnym złym nawykiem dotykania kodu, który nie powinien być.

Mówiąc dokładniej, wiedz o tym:

Istnieje coś takiego jak Over-Engineering

Jest to anty-wzorzec i zawiera wbudowane problemy:

  • to może być bardziej rozciągliwe , ale to nie może być łatwiejsze do rozszerzenia,
  • to nie może być prostsze do zrozumienia ,
  • na koniec, ale zdecydowanie nie tylko tutaj: możesz spowolnić cały kod.

Niektórzy mogą również wspomnieć o zasadzie KISS jako odniesienie, ale tutaj jest to sprzeczne z intuicją: czy zoptymalizowany sposób jest prosty, czy przejrzysty? Odpowiedź niekoniecznie musi być absolutna, jak wyjaśniono w dalszej części poniżej.

Nie będziesz go potrzebować

Zasada YAGNI nie jest całkowicie ortogonalna w stosunku do drugiego problemu, ale pomaga zadać sobie pytanie: czy będziesz go potrzebować?

Czy bardziej złożona architektura naprawdę stanowi dla Ciebie korzyść, poza tym, że wygląda na łatwiejszą do utrzymania?

Jeśli to nie jest zepsute, nie naprawiaj go

Napisz to na dużym plakacie i powieś obok ekranu, w kuchni w pracy lub w sali spotkań deweloperów. Oczywiście istnieje wiele innych mantr, które warto powtórzyć, ale ta konkretna jest ważna, gdy próbujesz wykonać „prace konserwacyjne” i odczuwasz potrzebę jej „ulepszenia”.

To naturalne, że chcemy „ulepszyć” kod, a nawet dotknąć go, nawet nieświadomie, kiedy go czytamy, aby go zrozumieć. To dobra rzecz, ponieważ oznacza to, że jesteśmy przekonani i staramy się lepiej zrozumieć elementy wewnętrzne, ale wiąże się to również z naszym poziomem umiejętności, naszą wiedzą (jak zdecydować, co jest lepsze, czy nie? Cóż, zobacz sekcje poniżej ...) i wszystkie nasze założenia dotyczące tego, co uważamy za oprogramowanie ...:

  • tak naprawdę
  • faktycznie musi zrobić
  • w końcu będzie musiał to zrobić,
  • i jak dobrze to robi.

Czy to naprawdę wymaga optymalizacji?

Wszystko to mówiło, dlaczego w ogóle „zoptymalizowano”? Mówią, że przedwczesna optymalizacja jest źródłem wszelkiego zła, a jeśli widzisz nieudokumentowany i pozornie zoptymalizowany kod, zwykle możesz założyć, że prawdopodobnie nie postępował zgodnie z Regułami optymalizacji, nie wymagał on wysiłku optymalizacyjnego i że był to po prostu zaczyna się zwykła deweloperka. Po raz kolejny, być może to teraz twoja rozmowa.

Jeśli tak, w jakim zakresie staje się to do przyjęcia? Jeśli zachodzi taka potrzeba, limit ten istnieje i daje ci miejsce na ulepszenie rzeczy lub na twardą linię, by zdecydować się na to.

Uważaj również na niewidzialne cechy. Możliwe, że twoja „rozszerzalna” wersja tego kodu również zwiększy ilość pamięci w czasie wykonywania, i zapewni jeszcze większy statyczny ślad pamięci dla pliku wykonywalnego. Błyszczące funkcje OO wiążą się z nieintuicyjnymi kosztami, takimi jak te, i mogą mieć znaczenie dla Twojego programu i środowiska, w którym ma działać.

Mierz, mierz, mierz

Jako ludzie Google teraz chodzi o dane! Jeśli możesz wykonać kopię zapasową danych, jest to konieczne.

Jest taka nie tak stara opowieść, że za każdy 1 USD wydany na rozwój będzie towarzyszył co najmniej 1 USD na testowanie i co najmniej 1 USD na wsparcie (ale tak naprawdę, to znacznie więcej).

Zmiana wpływa na wiele rzeczy:

  • może być konieczne wyprodukowanie nowej wersji;
  • powinieneś napisać nowe testy jednostkowe (zdecydowanie, gdyby ich nie było, a twoja bardziej rozszerzalna architektura prawdopodobnie pozostawia miejsce na więcej, ponieważ masz więcej powierzchni na błędy);
  • powinieneś napisać nowe testy wydajności (aby upewnić się, że pozostaną one stabilne w przyszłości i zobaczyć, gdzie są wąskie gardła), a te są trudne do wykonania ;
  • musisz to udokumentować (a większa rozszerzalność oznacza więcej miejsca na szczegóły);
  • Ty (lub ktoś inny) będziesz musiał dokładnie przetestować go w ramach kontroli jakości;
  • kod (prawie) nigdy nie jest wolny od błędów i musisz go obsługiwać.

Dlatego nie tylko zużycie zasobów sprzętowych (szybkość wykonywania lub wielkość pamięci) należy zmierzyć, ale także zużycie zasobów zespołu . Oba należy przewidzieć, aby określić cel docelowy, który należy zmierzyć, uwzględnić i dostosować w zależności od rozwoju.

A dla twojego menedżera, oznacza to dopasowanie go do obecnego planu rozwoju, więc komunikuj się o tym i nie wdawaj się w wściekłe kodowanie krowa-chłopca / łodzi podwodnej / black-opsa.


Ogólnie...

Tak ale...

Nie zrozumcie mnie źle, ogólnie rzecz biorąc, byłbym zwolennikiem wyjaśnienia, dlaczego to sugerujecie, i często to zalecam. Ale musisz zdawać sobie sprawę z długoterminowych kosztów.

W idealnym świecie jest to właściwe rozwiązanie:

  • sprzęt komputerowy z czasem staje się lepszy,
  • z czasem kompilatory i platformy uruchomieniowe stają się lepsze,
  • otrzymujesz prawie idealny, czysty, łatwy w utrzymaniu i czytelny kod.

W praktyce:

  • możesz to pogorszyć

    Potrzebujesz więcej gałek ocznych, aby na to spojrzeć, a im bardziej ją skomplikujesz, tym więcej potrzebujesz gałek ocznych.

  • nie możesz przewidzieć przyszłości

    Nie możesz z absolutną pewnością stwierdzić, czy kiedykolwiek będziesz go potrzebować, nawet jeśli „rozszerzenia”, których będziesz potrzebować, byłyby łatwiejsze i szybsze do wdrożenia w starej formie, a jeśli same wymagałyby superoptymalizacji .

  • z punktu widzenia zarządzania stanowi ogromny koszt bez bezpośredniego zysku.

Włącz to do procesu

Wspominasz tutaj, że jest to niewielka zmiana i masz na myśli pewne konkretne problemy. Powiedziałbym, że w tym przypadku zwykle jest OK, ale większość z nas ma również osobiste historie o drobnych zmianach, prawie modyfikacjach po chirurgicznym uderzeniu, które ostatecznie zamieniły się w koszmar utrzymania i prawie nie dotrzymały terminów lub wybuchły, ponieważ Joe Programmer nie widział z powodów stojących za kodem i dotknął czegoś, co nie powinno być.

Jeśli masz proces radzenia sobie z takimi decyzjami, pozbawiasz ich osobistej przewagi:

  • Jeśli przetestujesz wszystko poprawnie, będziesz wiedzieć szybciej, jeśli coś się zepsuje,
  • Jeśli je zmierzysz, będziesz wiedział, czy się poprawiły,
  • Jeśli go przejrzysz, dowiesz się, czy to zniechęca ludzi.

Pokrycie testowe, profilowanie i gromadzenie danych są trudne

Ale, oczywiście, twój kod testowy i metryki mogą mieć problemy z tymi samymi problemami, których starasz się unikać w swoim prawdziwym kodzie: czy testujesz właściwe rzeczy i czy są one właściwe na przyszłość i czy mierzysz właściwe rzeczy?

Jednak ogólnie rzecz biorąc, im więcej testujesz (aż do określonego limitu) i mierzysz, tym więcej danych zbierasz i tym bardziej jesteś bezpieczny. Zły czas na analogię: pomyśl o tym jak o prowadzeniu samochodu (lub o życiu): możesz być najlepszym kierowcą na świecie, jeśli samochód się zepsuje lub ktoś zdecyduje się zabić, wjeżdżając do twojego samochodu swoim własnym, swoim umiejętności mogą nie wystarczyć. Istnieją zarówno czynniki środowiskowe, które mogą cię uderzyć, jak i błędy ludzkie.

Recenzje kodu to testy korytarza zespołu programistów

I myślę, że ostatnia część jest tutaj kluczowa: wykonaj recenzje kodu. Nie poznasz wartości swoich ulepszeń, jeśli stworzysz je solo. Przeglądy kodu to nasze „testy korytarza”: postępuj zgodnie z wersją prawa Linusa Raymonda, zarówno w celu wykrycia błędów, jak i wykrycia nadmiernej inżynierii i innych anty-wzorów, oraz aby upewnić się, że kod jest zgodny ze zdolnościami twojego zespołu. Nie ma sensu mieć „najlepszego” kodu, jeśli nikt inny nie jest w stanie go zrozumieć i utrzymywać, a to dotyczy zarówno tajemniczych optymalizacji, jak i 6-warstwowych projektów architektonicznych.

Jako słowa końcowe pamiętaj:

Wszyscy wiedzą, że debugowanie jest dwa razy trudniejsze niż napisanie programu. Jeśli więc jesteś tak sprytny, jak tylko możesz, pisząc go, jak to będzie kiedykolwiek debugować? - Brian Kernighan


„Jeśli to nie zepsuło, nie naprawiaj” jest sprzeczne z refaktoryzacją. Nie ma znaczenia, czy coś działa, jeśli jest niemożliwe do utrzymania, należy je zmienić.
Miyamoto Akira

@MiyamotoAkira: jest to sprawa dwóch prędkości. Jeśli nie jest zepsuty, ale akceptowalny i rzadziej widzi wsparcie, może być dopuszczalne pozostawienie go w spokoju, zamiast wprowadzania potencjalnych nowych błędów lub poświęcania mu czasu na rozwój. Chodzi przede wszystkim o ocenę korzyści płynących z refaktoryzacji, zarówno krótko-, jak i długoterminowych. Nie ma jednoznacznej odpowiedzi, wymaga pewnej oceny.
haylem

Zgoda. Przypuszczam, że nie podoba mi się to zdanie (i filozofia za nim stojąca), ponieważ uważam refaktoryzację za domyślną opcję i tylko jeśli wydaje się, że zajmie to zbyt długo lub zbyt trudno, wtedy / należy zdecydować, aby nie idź z tym. Pamiętajcie, że zostałem spalony przez ludzi, którzy nie zmieniali rzeczy, które, choć działały, były wyraźnie złym rozwiązaniem, gdy tylko trzeba było je utrzymać lub rozszerzyć.
Miyamoto Akira

@MiyamotoAkira: krótkie zdania i wypowiedzi nie mogą wiele wyrazić. Mają być na twojej twarzy i chyba rozwinięte z boku. Jestem bardzo zaangażowany w przeglądanie i dotykanie kodu tak często, jak to możliwe, nawet jeśli często bez dużej siatki bezpieczeństwa lub bez powodu. Jeśli jest brudny, wyczyść go. Ale podobnie też kilka razy się poparzyłem. I nadal się poparzy. Tak długo, jak nie są to 3 stopnie, nie mam nic przeciwko, jak dotąd, to zawsze były krótkotrwałe oparzenia dla długoterminowych korzyści.
haylem

8

Ogólnie rzecz biorąc, powinieneś skupić się przede wszystkim na czytelności, a wydajności znacznie później. W większości przypadków te optymalizacje wydajności i tak są znikome, ale koszty utrzymania mogą być ogromne.

Z pewnością wszystkie „małe” rzeczy powinny zostać zmienione na korzyść przejrzystości, ponieważ, jak zauważyłeś, większość z nich zostanie zoptymalizowana przez kompilator.

Jeśli chodzi o większe optymalizacje, może istnieć szansa, że ​​optymalizacje są tak naprawdę kluczowe dla osiągnięcia rozsądnej wydajności (choć nie zdarza się to zaskakująco często). Dokonałbym twoich zmian, a następnie profilowałbym kod przed i po zmianach. Jeśli nowy kod ma poważne problemy z wydajnością, zawsze możesz przywrócić wersję zoptymalizowaną, a jeśli nie, możesz po prostu trzymać się czystszej wersji kodu.

Zmień tylko jedną część kodu na raz i zobacz, jak wpływa to na wydajność po każdej rundzie refaktoryzacji.


8

Zależy to od tego, dlaczego kod został zoptymalizowany i jaki byłby wpływ jego zmiany oraz jaki mógłby być wpływ kodu na ogólną wydajność. Powinno to również zależeć od tego, czy masz dobry sposób na załadowanie zmian testowych.

Nie powinieneś wprowadzać tej zmiany bez profilowania przed i po, a najlepiej pod obciążeniem podobnym do tego, co można zobaczyć na produkcji. Oznacza to, że nie używasz niewielkiego podzbioru danych na komputerze programisty ani nie testujesz, gdy z systemu korzysta tylko jeden użytkownik.

Jeśli optymalizacja była ostatnia, być może będziesz mógł porozmawiać z programistą i dowiedzieć się dokładnie, na czym polegał problem i jak powolna była aplikacja przed optymalizacją. To może wiele powiedzieć o tym, czy warto przeprowadzić optymalizację i jakie warunki była potrzebna optymalizacja (na przykład raport obejmujący cały rok może nie być spowolniony do września lub października, jeśli testujesz zmianę w lutym spowolnienie może jeszcze nie być widoczne, a test nieważny).

Jeśli optymalizacja jest raczej stara, nowsze metody mogą być nawet szybsze i bardziej czytelne.

Ostatecznie jest to pytanie do twojego szefa. Zmodernizowanie czegoś, co zostało zoptymalizowane, jest czasochłonne i upewnienie się, że zmiana nie wpłynęła na wynik końcowy i że działa on równie dobrze, a przynajmniej akceptowalnie w porównaniu ze starym sposobem. Może chcesz, abyś spędził czas w innych obszarach zamiast podejmować zadanie wysokiego ryzyka, aby zaoszczędzić kilka minut na kodowaniu. Lub może zgodzić się, że kod jest trudny do zrozumienia i wymagał częstej interwencji oraz że dostępne są teraz lepsze metody.


6

jeśli profilowanie pokazuje, że optymalizacja nie jest potrzebna (nie znajduje się w sekcji krytycznej) lub nawet ma gorszy czas działania (w wyniku złej przedwczesnej optymalizacji), wówczas należy zastąpić ją czytelnym kodem, który jest łatwiejszy do utrzymania

upewnij się również, że kod zachowuje się tak samo przy odpowiednich testach


5

Pomyśl o tym z perspektywy biznesowej. Jakie są koszty zmiany? Ile czasu potrzebujesz na wprowadzenie zmian i ile zaoszczędzisz na dłuższą metę, czyniąc kod łatwiejszym do rozszerzenia lub utrzymania? Teraz dołącz etykietę z ceną do tego czasu i porównaj ją z pieniędzmi utraconymi przez zmniejszenie wydajności. Być może trzeba dodać lub zaktualizować serwer, aby zrekompensować utratę wydajności. Być może produkt nie spełnia już wymagań i nie może być już sprzedawany. Może nie ma strat. Być może zmiana ta zwiększa niezawodność i oszczędza czas w innym miejscu. Teraz podejmij decyzję.

Na marginesie, w niektórych przypadkach może być możliwe zachowanie obu wersji fragmentu. Możesz napisać test generujący losowe wartości wejściowe i zweryfikować wyniki w innej wersji. Użyj „sprytnego” rozwiązania, aby sprawdzić wynik doskonale zrozumiałego i oczywiście poprawnego rozwiązania, a tym samym uzyskać pewne zapewnienie (ale bez dowodu), że nowe rozwiązanie jest równoważne ze starym. Lub odwrotnie i sprawdź wynik podstępnego kodu za pomocą pełnego kodu, a tym samym jednoznacznie udokumentuj zamiar włamania.


4

Zasadniczo pytasz, czy refaktoryzacja jest opłacalnym przedsięwzięciem. Odpowiedź na to pytanie jest z pewnością tak.

Ale...

... musisz to zrobić ostrożnie. Potrzebne są solidne testy integracyjne, funkcjonalne i wydajnościowe dla każdego kodu, który jest refaktoryzowany. Musisz mieć pewność, że naprawdę sprawdzają wszystkie wymagane funkcje. Potrzebujesz możliwości łatwego i powtarzalnego uruchamiania ich. Gdy to zrobisz, powinieneś być w stanie zastąpić komponenty nowymi komponentami zawierającymi równoważną funkcjonalność.

Martin Fowler napisał o tym książkę .


3

Nie należy zmieniać działającego kodu produkcyjnego bez uzasadnionego powodu. „Refaktoryzacja” nie jest wystarczającym powodem, chyba że nie można wykonać swojej pracy bez tego refaktoryzacji. Nawet jeśli naprawiasz błędy w samym trudnym kodzie, powinieneś poświęcić trochę czasu na jego zrozumienie i dokonać jak najmniejszej zmiany. Jeśli kod jest trudny do zrozumienia, nie będziesz w stanie go w pełni zrozumieć, więc wszelkie wprowadzone zmiany będą miały nieprzewidywalne skutki uboczne - innymi słowy, błędy. Im większa zmiana, tym większe prawdopodobieństwo, że spowodujesz problemy.

Byłby wyjątek od tego: jeśli niezrozumiały kod zawierałby pełny zestaw testów jednostkowych, można go przefakturować. Ponieważ nigdy nie widziałem ani nie słyszałem o niezrozumiałym kodzie z pełnymi testami jednostkowymi, najpierw piszesz testy jednostkowe, uzyskujesz zgodę niezbędnych osób, że te testy jednostkowe faktycznie reprezentują to, co powinien robić kod, a następnie wprowadzasz zmiany w kodzie . Zrobiłem to raz lub dwa; jest to ból szyi i bardzo drogi, ale w końcu przynosi dobre wyniki.


3

Jeśli jest to tylko krótki fragment kodu, który robi coś stosunkowo prostego w trudny do zrozumienia sposób, zmienię „szybkie zrozumienie” w rozszerzonym komentarzu i / lub nieużywanej alternatywnej implementacji, takiej jak

#ifdef READABLE_ALT_IMPLEMENTATION

   double x=0;
   for(double n: summands)
     x += n;
   return x;

#else

   auto subsum = [&](int lb, int rb){
          double x=0;
          while(lb<rb)
            x += summands[lb++];
          return x;
        };
   double x_fin=0;
   for(double nsm: par_eval( subsum
                           , partitions(n_threads, 0, summands.size()) ) )
     x_fin += nsm;
   return x_fin;

#endif

3

Odpowiedź brzmi: bez utraty ogólności. Zawsze dodawaj nowoczesny kod, gdy widzisz trudny do odczytania kod, i usuwaj zły kod w większości przypadków. Korzystam z następującego procesu:

  1. Poszukaj testu wydajności i pomocniczych informacji profilowania. Jeśli nie ma testu wydajności, to, co można stwierdzić bez dowodów, można odrzucić bez dowodów. Upewnij się, że twój nowoczesny kod jest szybszy i usuń stary kod. Jeśli ktoś twierdzi (nawet ty), poproś go o napisanie kodu profilującego, aby udowodnić, który jest szybszy.
  2. Jeśli kod profilowania istnieje, napisz ten kod mimo wszystko. Nazwij to jakoś <function>_clean(). Następnie „ścigaj się” ze swoim kodem w stosunku do złego kodu. Jeśli twój kod jest lepszy, usuń stary kod.
  3. Jeśli stary kod jest szybszy, zostaw tam swój nowoczesny kod. Służy jako dobra dokumentacja tego, co ma zrobić drugi kod, a ponieważ istnieje kod „wyścigowy”, możesz go dalej uruchamiać w celu udokumentowania charakterystyki wydajności i różnic między dwiema ścieżkami. Możesz także przetestować jednostkę pod kątem różnic w zachowaniu kodu. Co ważne, nowoczesny kod pewnego dnia pokona „zoptymalizowany” kod, gwarantowany. Następnie możesz usunąć zły kod.

CO BYŁO DO OKAZANIA.


3

Gdybym mógł nauczyć świat jednej rzeczy (o oprogramowaniu) zanim umrę, nauczę to, że „Wydajność kontra X” to fałszywy dylemat.

Refaktoryzacja jest zwykle znana jako dobrodziejstwo dla czytelności i niezawodności, ale równie łatwo może wspierać optymalizację. Kiedy traktujesz poprawę wydajności jako serię refaktoryzacji, możesz uszanować Regułę Kempingową, jednocześnie przyspieszając aplikację. W rzeczywistości, przynajmniej moim zdaniem, jest to etycznie obowiązkowe.

Na przykład autor tego pytania napotkał zwariowany fragment kodu. Gdyby ta osoba czytała mój kod, odkryłaby, że zwariowana część ma 3-4 wiersze. Jest to metoda sama w sobie, a nazwa i opis metody wskazują, co robi metoda. Metoda zawierałaby 2-6 wierszy wbudowanych komentarzy opisujących JAK szalony kod otrzymuje prawidłową odpowiedź, pomimo jego wątpliwego wyglądu.

W ten sposób podzielony na segmenty możesz dowolnie wymieniać implementacje tej metody. Rzeczywiście, prawdopodobnie tak właśnie napisałem szaloną wersję na początek. Możesz spróbować, a przynajmniej zapytać o alternatywy. Przez większość czasu dowiadujesz się, że naiwna implementacja jest zauważalnie gorsza (zwykle kłopotam się tylko poprawą 2-10x), ale kompilatory i biblioteki zawsze się zmieniają i kto wie, co możesz dziś znaleźć, co nie było dostępne, kiedy funkcja została napisana?


Głównym kluczem do wydajności w wielu przypadkach jest to, aby kod wykonywał jak najwięcej pracy na sposoby, które można wykonać efektywnie. Jedną z rzeczy, które mnie denerwują w .NET jest to, że nie ma wydajnych mechanizmów np. Do kopiowania części jednej kolekcji do drugiej. Większość kolekcji przechowuje duże grupy kolejnych elementów (jeśli nie całość) w tablicach, więc np. Skopiowanie ostatnich 5 000 pozycji z listy 50 000 elementów powinno rozpadać się na kilka operacji kopiowania zbiorczego (jeśli nie tylko jednej) plus kilka innych kroki zrobione najwyżej kilka razy.
supercat

Niestety, nawet w przypadkach, w których powinno być możliwe wydajne wykonywanie takich operacji, często konieczne będzie uruchomienie „nieporęcznych” pętli przez 5000 iteracji (a w niektórych przypadkach 45 000!). Jeśli operację można zredukować do takich rzeczy, jak kopie tablic zbiorczych, można je zoptymalizować do skrajnych stopni, co przyniesie znaczne korzyści. Jeśli każda iteracja pętli wymaga kilkunastu kroków, trudno jest szczególnie dobrze zoptymalizować którąkolwiek z nich.
supercat

2

Prawdopodobnie nie jest dobrym pomysłem, aby go dotknąć - jeśli kod został napisany w ten sposób ze względu na wydajność, oznacza to, że jego zmiana może przywrócić wcześniej rozwiązane problemy z wydajnością.

Jeśli nie zdecydujesz się zmienić ten stan rzeczy, aby być bardziej czytelne i rozszerzalne: Przed dokonaniem zmiany, porównywanie starego kodu pod dużym obciążeniem. Jeszcze lepiej, jeśli znajdziesz stary dokument lub zgłoszenie problemu opisujące problem z wydajnością, który ten dziwnie wyglądający kod powinien naprawić. Następnie po wprowadzeniu zmian ponownie uruchom testy wydajności. Jeśli nie jest bardzo różny lub nadal mieści się w akceptowalnych parametrach, prawdopodobnie jest to w porządku.

Czasami może się zdarzyć, że gdy zmieniają się inne części systemu, ten zoptymalizowany pod kątem wydajności kod nie wymaga już tak dużych optymalizacji, ale nie ma sposobu, aby wiedzieć na pewno bez rygorystycznych testów.


1
Jeden z facetów, z którymi pracuję, uwielbia optymalizować rzeczy w obszarach, które użytkownicy trafiają raz w miesiącu, jeśli to często. Zajmuje to czasu i nierzadko powoduje inne problemy, ponieważ lubi kodować i zatwierdzać oraz pozwolić, aby QA lub inna funkcja downstream faktycznie testowała. : / Mówiąc szczerze, jest on ogólnie szybki, szybki i dokładny, ale te „optymalizacje” za grosze tylko utrudniają resztę drużyny, a ich ciągła śmierć byłaby Dobrą Rzeczą.
DaveE

@DaveE: Czy te optymalizacje są stosowane z powodu rzeczywistych problemów z wydajnością? A może ten programista robi to tylko dlatego, że może? Myślę, że jeśli wiesz, że optymalizacje nie będą miały wpływu, możesz bezpiecznie zastąpić je bardziej czytelnym kodem, ale zaufałbym tylko komuś, kto jest ekspertem w tym zakresie.
FrustratedWithFormsDesigner

Skończyli, bo może. W rzeczywistości zwykle zapisuje kilka cykli, ale kiedy interakcja użytkownika z elementem programu zajmuje pewną liczbę sekund (od 15 do 300), golenie jednej dziesiątej sekundy czasu wykonywania w dążeniu do „wydajności” jest głupie. Zwłaszcza, gdy śledzący go ludzie muszą poświęcić czas na zrozumienie tego, co zrobił. Jest to aplikacja PowerBuilder pierwotnie zbudowana 16 lat temu, więc biorąc pod uwagę genezę sposobu myślenia, być może jest zrozumiały, ale odmawia aktualizacji swojego sposobu myślenia do obecnej rzeczywistości.
DaveE

@DaveE: Myślę, że bardziej zgadzam się z facetem, z którym pracujesz niż z tobą. Jeśli nie wolno mi naprawić rzeczy, które są powolne bez absolutnie żadnego dobrego powodu , oszaleję. Jeśli widzę wiersz C ++, który wielokrotnie używa operatora + do złożenia łańcucha, lub kod, który otwiera się i odczytuje / dev / urandom za każdym razem przez pętlę tylko dlatego, że ktoś zapomniał ustawić flagę, to naprawiam ją. Będąc fanatycznym w tej kwestii, udało mi się przyspieszyć, gdy inni ludzie pozwalali mu przesuwać się o jedną mikrosekundę na raz.
Zan Lynx,

1
Musimy się zgodzić, że się nie zgodzimy. Poświęcenie godziny na zmianę czegoś w celu zaoszczędzenia ułamkowych sekund w czasie wykonywania dla funkcji, która wykonuje się naprawdę okazjonalnie, i pozostawienie kodu w porywającym kształcie dla innych programistów jest ... nie w porządku. Gdyby były to funkcje, które wielokrotnie działały w częściach aplikacji o wysokim obciążeniu, fine & dandy. Ale nie tak opisuję. To naprawdę nieuzasadnione dodawanie kodu bez żadnego innego powodu niż powiedzenie „Zrobiłem to, co UserX robi raz w tygodniu ułamkowo szybciej”. W międzyczasie mamy płatną pracę, która wymaga wykonania.
DaveE

2

Problem polega na odróżnieniu „zoptymalizowanego” od czytelnego i rozszerzalnego, to, co my jako użytkownicy postrzegamy jako zoptymalizowany kod, a to, co kompilator postrzega jako zoptymalizowany, to dwie różne rzeczy. Kod, który chcesz zmienić, może wcale nie być wąskim gardłem, dlatego nawet jeśli kod jest „ubogi”, nie trzeba nawet „optymalizować”. Lub jeśli kod jest wystarczająco stary, kompilator może wprowadzić optymalizacje do wbudowanych, które sprawiają, że używanie nowszej prostej wbudowanej struktury jest równie lub bardziej wydajne niż stary kod.

A „chudy”, nieczytelny kod nie zawsze jest zoptymalizowany.

Kiedyś byłem przekonany, że sprytny / szczupły kod był dobrym kodem, ale czasami korzystając z niejasnych reguł języka boli raczej niż pomaga w tworzeniu kodu, gryzłem się częściej niż nie w żadnej pracy osadzonej, gdy próbowałem bądź sprytny, ponieważ kompilator zamienia twój sprytny kod w coś całkowicie bezużytecznego przez wbudowany sprzęt.


2

Nigdy nie zamienię kodu zoptymalizowanego na kod czytelny, ponieważ nie mogę iść na kompromis z wydajnością i wybiorę stosowanie odpowiedniego komentowania w każdej sekcji, aby każdy mógł zrozumieć logikę zaimplementowaną w tej sekcji zoptymalizowanej, która rozwiąże oba problemy.

Dlatego też Kod zostanie zoptymalizowany + Właściwe komentowanie sprawi, że będzie on także czytelny.

UWAGA: Możesz sprawić, by Zoptymalizowany Kod był czytelny za pomocą odpowiedniego komentowania, ale nie możesz ustawić Czytelnego Kodu jako Zoptymalizowany.


Byłbym zmęczony tym podejściem, ponieważ wystarczy jedna osoba edytująca kod, aby zapomnieć o synchronizacji komentarza. Nagle każda kolejna recenzja odchodzi, myśląc, że wykonuje X, podczas gdy faktycznie wykonuje Y.
John D

2

Oto przykład, aby zobaczyć różnicę między kodem prostym a kodem zoptymalizowanym: https://stackoverflow.com/a/11227902/1396264

pod koniec odpowiedzi po prostu zastępuje:

if (data[c] >= 128)
    sum += data[c];

z:

int t = (data[c] - 128) >> 31;
sum += ~t & data[c];

Aby być uczciwym, nie mam pojęcia, co zastąpiono instrukcją if, ale jak odpowiadający mówi, że niektóre bitowe operacje dają ten sam rezultat (po prostu uwierzę mu na słowo) .

Wykonuje się to w czasie krótszym niż jedna czwarta pierwotnego czasu (11,54 sek. Względem 2,5 sek.)


1

Główne pytanie tutaj: czy wymagana jest optymalizacja?

Jeśli tak, to nie możesz zastąpić go wolniejszym, bardziej czytelnym kodem. Musisz dodać do niego komentarze itp., Aby był bardziej czytelny.

Jeśli kod nie musi być optymalizowany, nie powinien być (do tego stopnia, że ​​wpływa na czytelność) i można go ponownie rozłożyć na czynniki, aby był bardziej czytelny.

JEDNAK - upewnij się, że dokładnie wiesz, co robi kod i jak dokładnie go przetestować, zanim zaczniesz coś zmieniać. Obejmuje to użycie szczytowe itp. Jeśli nie musisz tworzyć zestawu przypadków testowych i uruchamiać ich przed, a potem, to nie masz czasu na refaktoryzację.


1

W ten sposób robię różne rzeczy: najpierw uruchamiam go w czytelnym kodzie, a następnie optymalizuję. Zachowuję oryginalne źródło i dokumentuję kroki optymalizacji.

Następnie, gdy muszę dodać funkcję, wracam do mojego czytelnego kodu, dodaj tę funkcję i postępuj zgodnie z udokumentowanymi krokami optymalizacji. Ponieważ udokumentowałeś, bardzo szybko i łatwo można zoptymalizować kod dzięki nowej funkcji.


0

Czytelność IMHO jest ważniejsza niż zoptymalizowany kod, ponieważ w większości przypadków mikrooptymalizacja nie jest tego warta.

Artykuł o nieoptymalnych mikrooptymalizacjach :

Jak większość z nas, mam dość czytania postów na blogu o niesensownych mikrooptymalizacjach, takich jak zamiana print na echo, ++ $ i na $ i ++ lub podwójne cudzysłowy na pojedyncze cudzysłowy. Dlaczego? Ponieważ 99,999999% czasu jest nieistotne.

„print” używa jeszcze jednego kodu operacyjnego niż „echo”, ponieważ faktycznie coś zwraca. Możemy stwierdzić, że echo jest szybsze niż drukowanie. Ale jeden kod operacyjny nic nie kosztuje, naprawdę nic.

Próbowałem na świeżej instalacji WordPress. Skrypt zatrzymuje się, zanim zakończy się z błędem „Bus Error” na moim laptopie, ale liczba kodów operacyjnych wynosiła już ponad 2,3 miliona. Wystarczająco powiedziane.


0

Optymalizacja jest względna. Na przykład:

Rozważ klasę z grupą członków BOOL:

// no nitpicking over BOOL vs bool allowed
class Pear {
 ...
 BOOL m_peeled;
 BOOL m_sliced;
 BOOL m_pitted;
 BOOL m_rotten;
 ...
};

Może masz ochotę przekonwertować pola BOOL na pola bitowe:

class Pear {
 ...
 BOOL m_peeled:1;
 BOOL m_sliced:1;
 BOOL m_pitted:1;
 BOOL m_rotten:1;
 ...
};

Ponieważ BOOL ma typ INT (który na platformach Windows jest 32-bitową liczbą całkowitą ze znakiem), zajmuje to szesnaście bajtów i pakuje je w jeden. To 93% oszczędności! Kto może na to narzekać?

To założenie:

Ponieważ BOOL ma typ INT (który na platformach Windows jest 32-bitową liczbą całkowitą ze znakiem), zajmuje to szesnaście bajtów i pakuje je w jeden. To 93% oszczędności! Kto może na to narzekać?

prowadzi do:

Przekształcenie BOOL w pole jednobitowe pozwoliło zaoszczędzić trzy bajty danych, ale kosztowało osiem bajtów kodu, gdy element członkowski ma przypisaną wartość inną niż stała. Podobnie wyodrębnienie wartości staje się droższe.

Co było kiedyś

 push [ebx+01Ch]      ; m_sliced
 call _Something@4    ; Something(m_sliced);

staje się

 mov  ecx, [ebx+01Ch] ; load bitfield value
 shl  ecx, 30         ; put bit at top
 sar  ecx, 31         ; move down and sign extend
 push ecx
 call _Something@4    ; Something(m_sliced);

Wersja Bitfield jest większa o dziewięć bajtów.

Usiądźmy i zróbmy arytmetykę. Załóżmy, że do każdego z tych pól bitowych uzyskuje się dostęp sześć razy w kodzie, trzy razy dla zapisu i trzy razy dla odczytu. Koszt wzrostu kodu wynosi około 100 bajtów. Nie będzie to dokładnie 102 bajtów, ponieważ optymalizator może być w stanie wykorzystać wartości już w rejestrach dla niektórych operacji, a dodatkowe instrukcje mogą mieć ukryte koszty pod względem zmniejszonej elastyczności rejestru. Rzeczywista różnica może być większa, może być mniejsza, ale dla obliczenia z tyłu koperty nazwijmy to 100. Tymczasem oszczędność pamięci wynosiła 15 bajtów na klasę. Dlatego próg rentowności wynosi siedem. Jeśli Twój program tworzy mniej niż siedem instancji tej klasy, wówczas koszt kodu przekracza oszczędności danych: Optymalizacja pamięci była deoptymalizacją pamięci.

Bibliografia

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.