Czysty, czytelny kod vs szybki, trudny do odczytania kod. Kiedy przekroczyć linię?

67

Kiedy piszę kod, zawsze staram się, aby mój kod był jak najbardziej czysty i czytelny.

Od czasu do czasu przychodzi czas, kiedy trzeba przekroczyć linię i przejść od ładnego czystego kodu do nieco brzydszego kodu, aby był szybszy.

Kiedy można przekroczyć tę linię?

Ken Cochrane
źródło
69
Odpowiedziałeś na własne pytanie, przekraczasz linię, kiedy musisz przekroczyć linię
gnibbler
6
Twój „brudny kod” może działać równie szybko, jak „czysty kod” na sprzęcie za 6 miesięcy. Nie przesadzaj jednak, tak jak Windows. :)
Mateen Ulhaq,
21
Istnieje znacząca różnica między trudnym do zrozumienia algorytmem a trudnym do zrozumienia kodem. Czasami algorytm, który trzeba wdrożyć, jest skomplikowany, a kod z konieczności będzie mylący, po prostu dlatego, że wyraża złożony pomysł. Ale jeśli sam kod jest trudnym punktem, należy go naprawić.
tylerl
8
W wielu przypadkach inteligentny kompilator / interpreter może zoptymalizować czysty, czytelny kod, dzięki czemu ma taką samą wydajność jak kod „brzydki”. Nie ma więc wymówki, chyba że profilowanie mówi inaczej.
Dan Diplo,
1
Jeśli chodzi o kompilatory, twój brzydki kod najprawdopodobniej będzie taki sam jak czysty (zakładając, że nie robisz żadnych naprawdę dziwnych rzeczy). Zwłaszcza w .NET, to nie jest jak w C ++ / MFC dni, w których sposób definiowania zmiennych będzie miał wpływ na wydajność. Napisz konserwowalny kod. część kodu będzie po prostu złożona, ale to nie znaczy, że jest brzydka.
DustinDavis

Odpowiedzi:

118

Przekraczasz linię, kiedy

  • Zmierzyłeś, że twój kod jest zbyt wolny, aby mógł zostać użyty zgodnie z przeznaczeniem .
  • Wypróbowałeś alternatywne ulepszenia, które nie wymagają zepsucia kodu.

Oto prawdziwy przykład: uruchomiony przeze mnie eksperymentalny system zbyt wolno generował dane, zajmując ponad 9 godzin pracy i zużywając tylko 40% procesora. Zamiast zbytnio popsuć kod, przeniosłem wszystkie pliki tymczasowe do systemu plików w pamięci. Dodano 8 nowych wierszy nieprzyjemnego kodu, a teraz wykorzystanie procesora przekracza 98%. Problem rozwiązany; nie wymaga brzydoty.

Norman Ramsey
źródło
2
Upewnij się również, że zachowałeś oryginalny, wolniejszy, czystszy kod, zarówno jako implementację referencyjną, jak i do wycofania się, gdy zmienia się sprzęt i twój szybszy, bardziej hakujący kod nie działa.
Paul R
4
@PaulR Jak zachować ten kod? W formie komentarzy? To źle, imo - komentarze stają się nieaktualne, nikt ich nie czyta, a jeśli widzę skomentowany kod, zwykle go usuwam - po to jest kontrola źródła. Imo, komentarz do metody wyjaśniającej, co robi.
Evgeni
5
@Eugene: Zwykle zachowuję oryginalną wersję procedury fooi zmieniam jej nazwę foo_ref- zwykle znajduje się ona bezpośrednio nad fooplikiem źródłowym. W mojej uprzęży testowej wzywam foooraz foo_refdo walidacji i pomiaru wydajności względnej.
Paul R
5
@Paul, jeśli to robisz, dobrym pomysłem może być nieudany test, jeśli zoptymalizowana wersja jest zawsze wolniejsza niż funkcja ref. może się to zdarzyć, jeśli założenia, które uczyniłeś, aby przyspieszyć, nie są już prawdziwe.
user1852503
57

To fałszywa dychotomia. Możesz sprawić, że kod będzie szybki i łatwy w utrzymaniu.

Sposób, w jaki to robisz, polega na pisaniu go czysto, szczególnie przy możliwie najprostszej strukturze danych.

Następnie dowiadujesz się, gdzie są upływy czasu (uruchamiając go, po napisaniu go, a nie wcześniej) i naprawiasz je jeden po drugim. (Oto przykład.)

Dodano: Zawsze słyszymy o kompromisach, prawda, takich jak kompromis między czasem a pamięcią, czy kompromis między szybkością a utrzymywalnością? Chociaż takie krzywe mogą istnieć, nie należy zakładać, że jakikolwiek program jest na krzywej , a nawet gdziekolwiek w jej pobliżu.

Każdy program, który znajduje się na krzywej, można łatwo (poprzez przekazanie go do pewnego rodzaju programatora) uczynić zarówno znacznie wolniejszym, jak i znacznie mniejszym w utrzymaniu, a wtedy nie będzie w pobliżu krzywej. Taki program ma wtedy dużo miejsca, aby był szybszy i łatwiejszy w utrzymaniu.

Z mojego doświadczenia wynika, że ​​zaczyna się wiele programów.

Mike Dunlavey
źródło
Zgadzam się. Rzeczywiście szybki kod, który nie jest czysty, w końcu zwolni, ponieważ nie można go poprawnie zmodyfikować.
edA-qa mort-ora-y
22
Ja nie zgadzam się, że jest to fałszywa dychotomia; IMO istnieją scenariusze, szczególnie w kodzie biblioteki (nie tyle w kodzie aplikacji ), w których podział jest bardzo realny. Zobacz moją odpowiedź, aby uzyskać więcej.
Marc Gravell
1
Marc, możesz link do odpowiedzi w komentarzu za pomocą adresu URL „link”. programmers.stackexchange.com/questions/89620/…
Znaleźliśmy czasy, w których musimy spróbować przyspieszyć działanie kodu. Ale po eksperymentach z profilerem w celu znalezienia najlepszego rozwiązania (gdyby kod stał się brzydki), nie oznacza to, że kod musi pozostać brzydki. Chodzi o znalezienie najlepszego rozwiązania, które na pierwszy rzut oka może nie wydawać się oczywiste, ale po znalezieniu zwykle można je zakodować w czysty sposób. Dlatego uważam, że jest to fałszywa dychotomia i tylko pretekst, aby nie posprzątać pokoju po zabawie z zabawkami. Mówię, zrób to i posprzątaj swój pokój.
Martin York,
Nie zgadzam się, że to fałszywa dychotomia. Ponieważ wykonuję wiele prac graficznych, najbardziej oczywistym przykładem są ciasne pętle graficzne: nie wiem, jak często się to robi, ale zwykle silniki gier napisane w C używały asemblera do renderowania rdzenia pętle, aby wycisnąć każdą ostatnią kroplę prędkości. To sprawia, że ​​myślę o sytuacjach, w których programujesz w Pythonie, ale używasz modułów napisanych w C ++. „Trudny do odczytania” jest zawsze względny; za każdym razem, gdy przechodzisz na język niższego poziomu w celu uzyskania szybkości, kod ten jest trudniejszy do odczytania niż reszta.
jhocking
31

W moim istnieniu OSS wykonuję wiele prac bibliotecznych mających na celu zwiększenie wydajności, które są ściśle związane ze strukturą danych osoby wywołującej (tj. Zewnętrzną względem biblioteki), bez (z założenia) mandatu dla typów przychodzących. Tutaj najlepszym sposobem na uczynienie tego performera jest metaprogramowanie, co (ponieważ jestem w .NET-land) oznacza emisje IL. To jakiś brzydki, brzydki kod, ale bardzo szybki.

W ten sposób z radością akceptuję kod biblioteki może być „brzydszy” niż kod aplikacji , po prostu dlatego, że ma mniejszą (lub może nie) kontrolę nad danymi wejściowymi , więc musi realizować niektóre zadania za pomocą różnych mechanizmów. Lub jak wyraziłem to innego dnia:

„koduje nad klifem szaleństwa, więc nie musisz

Teraz kod aplikacji jest nieco inny, ponieważ w tym miejscu „zwykli” (rozsądni) programiści zwykle inwestują dużą część swojego czasu pracy / współpracy; cele i oczekiwania każdego z nich są (IMO) nieco inne.

IMO, powyższe odpowiedzi sugerujące, że może być szybki i łatwy w utrzymaniu, odnoszą się do kodu aplikacji, w którym programista ma większą kontrolę nad strukturami danych i nie używa narzędzi takich jak metaprogramowanie. To powiedziawszy, istnieją różne sposoby wykonywania metaprogramowania, różne poziomy szaleństwa i różne poziomy kosztów ogólnych. Nawet na tej arenie musisz wybrać odpowiedni poziom abstrakcji. Ale kiedy aktywnie, pozytywnie, naprawdę chcesz, aby obsługiwał nieoczekiwane dane w absolutnie najszybszy sposób; to może stać się brzydkie. Sobie z tym poradzić; p

Marc Gravell
źródło
4
To, że kod jest brzydki, nie oznacza, że ​​musi być nie do utrzymania. Komentarze i wcięcia są bezpłatne, a brzydki kod może być zwykle zamknięty w zarządzalnej jednostce (klasa, moduł, pakiet, funkcja, w zależności od języka). Kod może być równie brzydki, ale wtedy przynajmniej ludzie będą w stanie ocenić wpływ zmian, które zamierzają wprowadzić.
tdammers,
6
@tdammers rzeczywiście i staram się to robić w miarę możliwości; ale to trochę jak nakładanie szminki na świnię.
Marc Gravell
1
Cóż, być może należy wprowadzić rozróżnienie między brzydką składnią i brzydkimi algorytmami - brzydkie algorytmy są czasem konieczne, ale brzydka składnia jest na ogół niewybaczalnym IMO.
tdammers,
4
Brzydka składnia @IMO jest nieunikniona, jeśli robisz z natury kilka poziomów abstrakcji poniżej zwykłego poziomu językowego.
Marc Gravell
1
@marc ... to interesujące. Moją pierwszą reakcją na brzydkość meta / abstrakcji było podejrzenie, że konkretny język / forma płyty nie sprzyja meta kodowaniu, a nie jakieś podstawowe prawo łączące te dwa elementy. Sprawiło, że uwierzyłem, że był to przykład progresywnych metaleveli w matematyce kończących się na teorii mnogości, których ekspresja nie jest wcale bardziej brzydka niż algebry czy nawet konkretnej arytmatyki. Ale wtedy zapis notacji jest prawdopodobnie zupełnie innym językiem, a każdy poziom abstracji ma swój własny język ...
explorest
26

Po sprofilowaniu kodu i upewnieniu się, że w rzeczywistości powoduje on znaczne spowolnienie.

Joel Rein
źródło
3
A co jest „znaczące”?
Rook
2
@ hotpaw2: to inteligentna odpowiedź - zakłada, że ​​programiści są co najmniej nieco konkurencyjni. W przeciwnym razie tak, użycie czegoś szybciej niż sortowanie bąbelkowe jest (zwykle) dobrym pomysłem. Ale zbyt często ktoś (aby zachować sortowanie) zamienia Quicksort na Heapsort z różnicą 1%, tylko po to, by zobaczyć, że ktoś inny zamieni go z powrotem sześć miesięcy później z tego samego powodu.
1
Jest nie powód, aby nie czysty kod. Jeśli nie możesz uczynić wydajnego kodu czystym i łatwym w utrzymaniu, robisz coś źle.
edA-qa mort-ora-y
2
@SF. - klient zawsze uzna, że ​​jest zbyt wolny, jeśli może być szybszy. Nie dba o „czysty” kod.
Wieża
1
@Rook: Klient może uznać (trywialny) kod interfejsu za wolny. Niektóre dość proste sztuczki psychologiczne poprawiają wrażenia użytkownika bez faktycznego przyspieszania kodu - odkładaj zdarzenia przycisków do rutyny w tle zamiast wykonywać czynności w locie, wyświetlać pasek postępu lub coś w tym rodzaju, zadawać mało znaczące pytania podczas wykonywania czynności w tle ... kiedy to nie wystarczy, możesz rozważyć faktyczną optymalizację.
SF.
13

Czysty kod niekoniecznie jest wyłączny w przypadku szybko wykonywanego kodu. Normalnie trudny do odczytania kod został napisany, ponieważ pisanie było szybsze, a nie dlatego, że działa szybciej.

Pisanie „brudnego” kodu w celu przyspieszenia go jest prawdopodobnie nierozsądne, ponieważ nie wiadomo na pewno, że wprowadzone zmiany rzeczywiście coś poprawiają. Knuth ujął to najlepiej:

„Powinniśmy zapomnieć o niewielkiej wydajności, powiedzmy w około 97% przypadków: przedwczesna optymalizacja jest źródłem wszelkiego zła . Jednak nie powinniśmy tracić naszych możliwości w tak krytycznych 3%. Dobry programista nie zostanie uśpiony przez samozadowolenie rozumując, mądrze będzie przyjrzeć się krytycznemu kodowi, ale dopiero po jego zidentyfikowaniu ”.

Innymi słowy, najpierw napisz kod czysty. Następnie profiluj wynikowy program i sprawdź, czy ten segment jest w rzeczywistości wąskim gardłem wydajności. Jeśli tak, zoptymalizuj sekcję, jeśli to konieczne, i pamiętaj o dołączeniu dużej ilości komentarzy do dokumentacji (być może łącznie z oryginalnym kodem) w celu wyjaśnienia optymalizacji. Następnie profiluj wynik, aby sprawdzić, czy rzeczywiście dokonałeś poprawy.

tylerl
źródło
10

Ponieważ pytanie brzmi „szybki, trudny do odczytania kod”, prosta odpowiedź nigdy nie jest taka. Nigdy nie ma usprawiedliwienia dla pisania trudnego do odczytania kodu. Dlaczego? Dwa powody.

  1. Co się stanie, jeśli wieczorem wieczorem wrócisz do autobusu? A może (bardziej optymistycznie i bardziej typowo) wycofałeś się z tego projektu i przypisałeś go do czegoś innego? Niewielka korzyść, jaką możesz sobie wyobrazić dzięki splątanemu bałaganowi kodu, całkowicie przeważa nad tym, że nikt inny tego nie rozumie . Ryzyko, jakie to stwarza dla projektów oprogramowania, jest trudne do przecenienia. Raz pracowałem z głównym PBXproducent (jeśli pracujesz w biurze, prawdopodobnie masz jeden z ich telefonów na biurku). Menedżer projektu powiedział mi pewnego dnia, że ​​ich podstawowy produkt - autorskie oprogramowanie, które zamieniło standardowy komputer z systemem Linux w pełni funkcjonalną centralę telefoniczną - był znany w firmie jako „kropelka”. Nikt tego już nie rozumiał. Za każdym razem wdrażali nową funkcję. uderzyli w kompilację, potem cofnęli się, zamknęli oczy, policzyli do dwudziestu, a następnie zerknęli przez palce, aby sprawdzić, czy zadziała. Żadna firma nie potrzebuje podstawowego produktu, którego już nie kontroluje, ale jest to przerażająco powszechny scenariusz.
  2. Ale muszę zoptymalizować! OK, więc zastosowałeś się do wszystkich doskonałych rad zawartych w innych odpowiedziach na to pytanie: Twój kod nie spełnia wymagań testów wydajności, dokładnie go profilowałeś, zidentyfikowałeś wąskie gardła, wymyśliłeś rozwiązanie ... i będzie wiązać się z trochę kręceniem się . Dobrze: teraz idź i zoptymalizuj. Ale oto sekret (i możesz usiąść przy tym): optymalizacja i zmniejszenie rozmiaru kodu źródłowego to nie to samo. Komentarze, białe znaki, nawiasy klamrowe i znaczące nazwy zmiennych to ogromne ułatwienia dla czytelności, które absolutnie nic nie kosztują, ponieważ kompilator je wyrzuci. (Lub jeśli piszesz nieskompilowany język, taki jak JavaScript - i tak, istnieją bardzo ważne powody, aby zoptymalizować JavaScript - można sobie z nimi poradzić za pomocą kompresora .) Długie linie ciasnego, minimalistycznego kodu (takiego jak ten, który ma muntoo zamieszczone tutaj ) nie mają nic wspólnego z optymalizacją: to programista próbuje pokazać, jak sprytny jest, pakując jak najwięcej kodu w jak najmniej znaków. To nie jest mądre, to głupie. Naprawdę sprytny programista to taki, który może jasno przekazywać swoje pomysły innym.
Mark Whitaker
źródło
2
Nie mogę się zgodzić, że odpowiedź brzmi „nigdy”. Niektóre algorytmy są z natury bardzo trudne do zrozumienia i / lub skutecznego wdrożenia. Czytanie kodu, niezależnie od liczby komentarzy, może być bardzo trudnym procesem.
Rex Kerr
4

Gdy jest to kod wyrzucany. To znaczy, że dosłownie: kiedy napisać skrypt, aby wykonać obliczenie jednorazowej lub zadanie, a wiem z taką pewnością, że nigdy nie będzie musiał zrobić to działanie ponownie, że można „rm source-file” bez wahania, po czym możesz wybierać brzydka trasa.

W przeciwnym razie jest to fałszywa dychotomia - jeśli uważasz, że musisz uczynić to brzydszym, aby zrobić to szybciej, robisz to źle. (Lub twoje zasady dotyczące tego, co jest dobrym kodem, wymagają korekty. Korzystanie z goto jest w rzeczywistości dość eleganckie, gdy jest właściwym rozwiązaniem problemu. Jednak rzadko tak jest.)

maaku
źródło
5
Nie ma czegoś takiego jak kod wyrzucający. Gdybym miał grosz za każdym razem, gdy „wyrzucony kod” wszedł do produkcji, ponieważ „działa, nie mamy czasu na jego przepisywanie”, byłbym milionerem. Każdy wiersz kodu, który piszesz, powinien być napisany w taki sposób, aby inny kompetentny programista mógł go odebrać jutro po uderzeniu pioruna. W przeciwnym razie nie pisz tego.
Mark Whitaker,
Nie zgadzam się, że to fałszywa dychotomia; IMO istnieją scenariusze, szczególnie w kodzie biblioteki (nie tyle w kodzie aplikacji), w których podział jest bardzo realny. Zobacz moją odpowiedź, aby uzyskać więcej.
Marc Gravell
@mark, jeśli „inny kompetentny programista” jest naprawdę kompetentny, to kod wyrzucania również nie powinien stanowić problemu :)
@ Mark - Easy. Po prostu napisz kod wyrzucający, aby nie przeszedł żadnego testu produkcyjnego, być może w jakiś niemożliwy do naprawienia sposób.
hotpaw2
@Zaznaczyć, jeśli „kod wyrzucania” włącza się do produkcji, nie jest to kod wyrzucania. Zauważ, że poświęciłem trochę czasu w swojej odpowiedzi, aby wyjaśnić, że mówię o kodzie, który jest dosłownie wyrzucany: tj. Usuń po pierwszym użyciu. W przeciwnym razie zgadzam się z twoim sentymentem i powiedziałem tyle w mojej odpowiedzi.
maaku
3

Ilekroć szacunkowy koszt niższej wydajności na rynku jest większy niż szacowany koszt utrzymania kodu dla danego modułu kodu.

Ludzie wciąż robią skręcone ręcznie kodowane SSE / NEON / itp. montaż, aby spróbować pokonać oprogramowanie konkurencji na popularnym tegorocznym chipie CPU.

hotpaw2
źródło
Dobra perspektywa biznesowa, czasami programiści muszą wychodzić poza kwestie techniczne.
this.josh
3

Nie zapominaj, że możesz uczynić trudny do odczytania kod łatwym do zrozumienia dzięki odpowiedniej dokumentacji i komentarzom.

Ogólnie rzecz biorąc, profil po napisaniu łatwego do odczytania kodu, który wykonuje żądaną funkcję. Wąskie gardła mogą wymagać od ciebie zrobienia czegoś, co sprawia, że ​​wygląda to bardziej skomplikowane, ale naprawiasz to poprzez wyjaśnienie siebie.

Carlos
źródło
0

Dla mnie jest to proporcja stabilności (jak w przypadku cementu w betonie, wypalanej w piecu gliny, osadzonej w kamieniu, zapisanej trwałym atramentem). Im bardziej niestabilny jest twój kod, ponieważ im większe prawdopodobieństwo, że będziesz musiał go zmienić w przyszłości, tym łatwiej będzie go elastycznie, podobnie jak mokra glina, aby pozostać produktywnym. Podkreślam także elastyczność i brak czytelności. Dla mnie łatwość zmiany kodu jest ważniejsza niż łatwość czytania. Kod może być łatwy do odczytania, a koszmar do zmiany, a jaki pożytek jest w stanie odczytać i łatwo zrozumieć szczegóły implementacji, jeśli koszmar do zmiany? O ile nie jest to tylko ćwiczenie akademickie, zwykle celem jest łatwe zrozumienie kodu w bazie kodu produkcyjnego z zamiarem łatwiejszej zmiany go w razie potrzeby. Jeśli trudno to zmienić, wtedy wiele zalet czytelności wychodzi poza okno. Czytelność jest ogólnie użyteczna tylko w kontekście elastyczności, a elastyczność jest użyteczna tylko w kontekście niestabilności.

Oczywiście nawet najtrudniejszy do utrzymania kod, jaki można sobie wyobrazić, bez względu na to, jak łatwy lub trudny jest jego odczyt, nie stanowi problemu, jeśli nigdy nie ma powodu, aby go zmieniać, tylko go używać. Możliwe jest osiągnięcie takiej jakości, szczególnie w przypadku niskiego poziomu kodu systemowego, w którym wydajność często liczy się najbardziej. Mam kod C, którego wciąż używam, co nie zmieniło się od późnych lat 80-tych. Od tego czasu nie musiał się zmieniać. Kod jest niezgrabny, napisany w czasach grzebania w bitach i ledwo go rozumiem. Nadal jednak ma zastosowanie do dzisiaj i nie muszę rozumieć jego implementacji, aby czerpać z niego wiele korzyści.

Dokładne pisanie testów jest jednym ze sposobów poprawy stabilności. Innym jest oddzielenie płatności od produkcji. Jeśli twój kod nie zależy od niczego innego, to jedynym powodem do zmiany jest to, że sam musi się zmienić. Czasami niewielka ilość duplikacji kodu może służyć jako mechanizm odsprzęgający, który radykalnie poprawia stabilność w sposób, który czyni go godnym kompromisem, jeśli w zamian otrzymujesz kod, który jest teraz całkowicie niezależny od wszystkiego innego. Teraz ten kod jest niewrażliwy na zmiany w świecie zewnętrznym. Tymczasem kod, który zależy od 10 różnych bibliotek zewnętrznych, ma 10-krotny powód do zmiany w przyszłości.

Inną przydatną rzeczą w praktyce jest oddzielenie biblioteki od niestabilnych części bazy kodu, być może nawet zbudowanie jej osobno, tak jak w przypadku bibliotek stron trzecich (które podobnie mają być używane, a nie zmieniane, a przynajmniej nie przez zespół). Tylko ten rodzaj organizacji może zapobiec manipulowaniu nią przez ludzi.

Kolejny to minimalizm. Im mniej Twój kod próbuje zrobić, tym większe prawdopodobieństwo, że potrafi zrobić to, co robi dobrze. Monolityczne konstrukcje są prawie trwale niestabilne, ponieważ im więcej funkcji jest dodawanych, tym bardziej wydają się niekompletne.

Stabilność powinna być Twoim głównym celem za każdym razem, gdy chcesz napisać kod, który nieuchronnie będzie trudny do zmiany, jak na przykład równoległy kod SIMD, który został dostrojony do mikroukładu. Przeciwdziałasz trudnościom w utrzymywaniu kodu, maksymalizując prawdopodobieństwo, że nie będziesz musiał go zmieniać, a zatem nie będziesz musiał go utrzymywać w przyszłości. To obniża koszty utrzymania do zera, bez względu na to, jak trudny jest kod.


źródło