Optymalizacja kodu: optymalny sposób optymalizacji
Opublikowany: 2022-03-11Optymalizacja wydajności to jedno z największych zagrożeń dla Twojego kodu.
Być może myślisz, a nie inna z tych osób . Rozumiem. Jakakolwiek optymalizacja powinna być oczywiście dobrą rzeczą, sądząc po jej etymologii, więc naturalnie chcesz być w tym dobry.
Nie tylko po to, by wyróżnić się z tłumu jako lepszy programista. Nie tylko po to, by uniknąć bycia „Danem” w The Daily WTF , ale dlatego, że uważasz, że optymalizacja kodu jest właściwą rzeczą do zrobienia. Jesteś dumny ze swojej pracy.
Sprzęt komputerowy staje się coraz szybszy, a oprogramowanie łatwiejsze do wykonania, ale bez względu na prostą rzecz, którą po prostu chcesz być w stanie zrobić, cholera zawsze zajmuje więcej czasu niż poprzednia. Kręcisz głową na to zjawisko (nawiasem mówiąc, znane jako prawo Wirtha) i postanawiasz przeciwstawić się temu trendowi.
To szlachetne z twojej strony, ale przestań.
Przestań!
Jesteś w największym niebezpieczeństwie udaremnienia własnych celów, bez względu na to, jak bardzo masz doświadczenie w programowaniu.
Jak to? Cofnijmy się.
Przede wszystkim, czym jest optymalizacja kodu?
Często, kiedy to definiujemy, zakładamy, że chcemy, aby kod działał lepiej. Mówimy, że optymalizacja kodu to pisanie lub przepisywanie kodu tak, aby program zużywał jak najmniej pamięci lub miejsca na dysku, minimalizował czas procesora lub przepustowość sieci lub jak najlepiej wykorzystywał dodatkowe rdzenie.
W praktyce czasami domyślnie przyjmujemy inną definicję: Pisanie mniej kodu.
Ale uprzedzająco zły kod, który piszesz w tym celu, jest jeszcze bardziej prawdopodobne, że stanie się cierniem w czyimś boku. Którego? Następna pechowa osoba, która musi zrozumieć Twój kod, którym możesz być nawet Ty. A ktoś mądry i zdolny, taki jak ty, może uniknąć samosabotażu: zachowaj szlachetne cele, ale ponownie oceń swoje środki, mimo że wydają się one bezsprzecznie intuicyjne.
Tak więc optymalizacja kodu jest trochę niejasnym terminem. To jest zanim rozważymy niektóre inne sposoby optymalizacji kodu, które opiszemy poniżej.
Zacznijmy od słuchania rad mędrców, gdy wspólnie badamy słynne zasady optymalizacji kodu Jacksona:
- Nie rób tego.
- (Tylko dla ekspertów!) Nie rób tego jeszcze .
1. Nie rób tego: kanalizowanie perfekcjonizmu
Zacznę od dość żenująco ekstremalnego przykładu z czasów, kiedy dawno temu właśnie moczyłem się w cudownym świecie SQL, który ma ochotę na ciastko i zjedz to też. Problem polegał na tym, że nadepnęłam na tort i nie chciałam go już jeść, bo było mokre i zaczęło pachnieć jak stopy.
Właśnie zmoczyłem stopy w cudownym świecie SQL, zjedz ciastko i jedz też. Problem polegał na tym, że nadepnęłam na tort…
Czekać. Pozwól mi wydostać się z tego wraku samochodu z metafory, którą właśnie stworzyłem i wyjaśnić.
Prowadziłem prace badawczo-rozwojowe nad aplikacją intranetową, która, jak miałem nadzieję, pewnego dnia stanie się całkowicie zintegrowanym systemem zarządzania dla małej firmy, w której pracowałem. Śledziłby wszystko za nich i w przeciwieństwie do ich ówczesnego systemu, nigdy nie utraciłby ich danych, ponieważ byłby wspierany przez RDBMS, a nie niestabilny, domowy plik płaski, którego używali inni programiści. Od samego początku chciałem zaprojektować wszystko tak sprytnie, jak to tylko możliwe, ponieważ miałem czystą kartę. Pomysły na ten system eksplodowały w mojej głowie jak fajerwerki i zacząłem projektować tabele — kontakty i ich wiele kontekstowych odmian dla CRM, modułów księgowych, zapasów, zakupów, CMS i zarządzania projektami, które wkrótce będę testować.
Że wszystko się zatrzymało, jeśli chodzi o rozwój i wydajność, z powodu… zgadliście, optymalizacji.
Zobaczyłem, że obiekty (reprezentowane jako wiersze tabeli) mogą mieć wiele różnych relacji między sobą w rzeczywistym świecie i że możemy odnieść korzyści ze śledzenia tych relacji: zachowalibyśmy więcej informacji i moglibyśmy ostatecznie zautomatyzować analizę biznesową w całym miejscu. Widząc to jako problem inżynierski, zrobiłem coś, co wydawało się optymalizacją elastyczności systemu.
W tym momencie ważne jest, aby uważać na swoją twarz, ponieważ nie będę ponosił odpowiedzialności, jeśli twoja dłoń ją zrani. Gotowy? Utworzyłem dwie tabele: relationship i jedną, do której miała odwołanie do klucza obcego, relationship_type . relationship może odnosić się do dowolnych dwóch wierszy w dowolnym miejscu całej bazy danych i opisywać charakter relacji między nimi.
O stary. Właśnie tak bardzo zoptymalizowałem tę elastyczność.
Właściwie za dużo. Teraz pojawił się nowy problem: dany typ relationship_type naturalnie nie miałby sensu między każdą podaną kombinacją wierszy. Chociaż może to mieć sens, że dana person miała stosunek employed by do company , nigdy nie może być semantycznie równoważny relacji między, powiedzmy, dwoma document .
Ok, nie ma problemu. Po prostu dodamy dwie kolumny do connection_type , określając, do których tabel można zastosować tę relationship_type . (Dodatkowe punkty tutaj, jeśli zgadujesz, że pomyślałem o normalizacji tego, przenosząc te dwie kolumny do nowej tabeli odnoszącej się do relationship_type.id , tak aby relacje, które mogą semantycznie dotyczyć więcej niż jednej pary tabel, nie miały zduplikowanych nazw tabel. W końcu, gdybym musiał zmienić nazwę tabeli i zapomniałem zaktualizować ją we wszystkich odpowiednich wierszach, mogłoby to stworzyć błąd! Z perspektywy czasu przynajmniej owady byłyby pokarmem dla pająków zamieszkujących moją czaszkę.)
Na szczęście straciłem przytomność podczas burzy ze wskazówkami, zanim zszedłem zbyt daleko tą ścieżką. Kiedy się obudziłem, zdałem sobie sprawę, że udało mi się mniej więcej ponownie zaimplementować wewnętrzne tabele RDBMS związane z kluczami obcymi. Normalnie lubię chwile, które kończą się chełpliwym głoszeniem „Jestem taki meta”, ale to niestety nie było jednym z nich. Zapomnij o niepowodzeniu w skalowaniu — straszliwe rozdęcie tego projektu sprawiło, że zaplecze mojej wciąż prostej aplikacji, której baza danych nie była jeszcze zapełniona żadnymi danymi testowymi, było prawie bezużyteczne.
Cofnijmy się na chwilę i przyjrzyjmy się dwóm z wielu wskaźników, które tutaj występują. Jednym z nich jest elastyczność, która była moim deklarowanym celem. W tym przypadku moja optymalizacja, mająca charakter architektoniczny, nie była nawet przedwczesna:
(Dojdziemy do tego więcej w moim niedawno opublikowanym artykule Jak uniknąć klątwy przedwczesnej optymalizacji.) Niemniej jednak moje rozwiązanie spektakularnie zawiodło, ponieważ było zbyt elastyczne. Druga metryka, skalowalność, była tą, której jeszcze nawet nie brałem pod uwagę, ale udało mi się zniszczyć co najmniej tak spektakularnie , uszkadzając ją pobocznie.
Zgadza się, „Och”.
To była dla mnie potężna lekcja na temat tego, jak optymalizacja może pójść nie tak. Mój perfekcjonizm całkowicie się załamał: mój spryt doprowadził mnie do stworzenia jednego z najbardziej obiektywnie niesprytnych rozwiązań, jakie kiedykolwiek zrobiłem.
Zoptymalizuj swoje nawyki, a nie swój kod
Kiedy zauważysz, że masz tendencję do refaktoryzacji, zanim jeszcze masz działający prototyp i zestaw testów, aby udowodnić jego poprawność, zastanów się, gdzie jeszcze możesz skierować ten impuls. Sudoku i Mensa są świetne, ale może coś, co bezpośrednio przyniesie korzyści Twojemu projektowi, byłoby lepsze:
- Bezpieczeństwo
- Stabilność działania
- Klarowność i styl
- Wydajność kodowania
- Testuj skuteczność
- Profilowy
- Twój zestaw narzędzi/DE
- SUCHE (nie powtarzaj się)
Ale uważaj: optymalizacja do cholery z jednego z nich będzie kosztem innych. Przynajmniej dzieje się to kosztem czasu.
W tym miejscu można łatwo zobaczyć, ile sztuki jest w tworzeniu kodu. W przypadku każdego z powyższych mogę opowiedzieć historie o tym, jak za dużo lub za mało uznano za zły wybór. Kto myśli tutaj, jest również ważną częścią kontekstu.
Na przykład w odniesieniu do DRY: W jednej pracy, którą miałem, odziedziczyłem bazę kodu, która zawierała co najmniej 80% zbędnych instrukcji, ponieważ jej autor najwyraźniej nie wiedział, jak i kiedy napisać funkcję. Pozostałe 20% kodu było łudząco podobne do siebie.
Miałem za zadanie dodać do niego kilka funkcji. Jedna taka funkcja musiałaby być powtarzana w całym kodzie, który ma zostać zaimplementowany, a każdy przyszły kod musiałby być starannie skopiowany , aby mógł wykorzystać nową funkcję.
Oczywiście wymagało to refaktoryzacji tylko dla mojego zdrowia psychicznego (wysoka wartość) i dla przyszłych deweloperów. Ale ponieważ byłem nowy w kodzie, najpierw napisałem testy, aby upewnić się, że moja refaktoryzacja nie wprowadziła żadnych regresji. W rzeczywistości zrobili właśnie to: wykryłem po drodze dwa błędy, których nie zauważyłbym pośród wszystkich żarłocznych danych wyjściowych, które wyprodukował skrypt.
W końcu pomyślałem, że poradziłem sobie całkiem nieźle. Po refaktoryzacji zaimponowałem szefowi, że zaimplementowałem to, co uważano za trudną, za pomocą kilku prostych linijek kodu; co więcej, kod był ogólnie o rząd wielkości bardziej wydajny. Ale niedługo po tym ten sam szef powiedział mi, że byłem zbyt wolny i że projekt powinien już się skończyć. Tłumaczenie: Efektywność kodowania miała wyższy priorytet.
Uwaga: optymalizacja do cholery z każdego konkretnego [aspektu] będzie kosztować innych. Przynajmniej dzieje się to kosztem czasu.
Nadal uważam, że obrałem właściwy kurs, nawet jeśli optymalizacja kodu nie była wówczas doceniana bezpośrednio przez mojego szefa. Myślę, że bez refaktoryzacji i testów zajęłoby więcej czasu, aby faktycznie uzyskać poprawność – tj. skupienie się na szybkości kodowania faktycznie by to udaremniło. (Hej, to nasz motyw!)
Porównaj to z pracą, którą wykonałem przy moim małym projekcie pobocznym. W projekcie próbowałem nowego silnika szablonów i od samego początku chciałem nabrać dobrych nawyków, mimo że wypróbowanie nowego silnika szablonów nie było celem końcowym projektu.
Gdy tylko zauważyłem, że kilka dodanych przeze mnie bloków jest do siebie bardzo podobnych, a ponadto każdy blok wymagał trzykrotnego odniesienia się do tej samej zmiennej, w mojej głowie zabrzmiał dzwonek DRY i zacząłem szukać właściwej sposób na zrobienie tego, co próbowałem zrobić z tym silnikiem szablonów.
Po kilku godzinach bezowocnego debugowania okazało się, że obecnie nie jest to możliwe z silnikiem szablonów w sposób, jaki sobie wyobrażałem. Nie tylko nie było idealnego rozwiązania DRY; nie było w ogóle żadnego rozwiązania SUCHEGO!
Próbując zoptymalizować tę jedną z moich wartości, całkowicie wykoleiłem moją wydajność kodowania i moje szczęście, ponieważ ten objazd kosztował mój projekt postęp, jaki mogłem osiągnąć tego dnia.
Czy nawet wtedy całkowicie się myliłem? Czasami warto trochę zainwestować, szczególnie w kontekście nowej technologii, aby poznać najlepsze praktyki wcześniej, a nie później. Mniej kodu do przepisania i złych nawyków do cofnięcia, prawda?
Nie, myślę, że nawet szukanie sposobu na zmniejszenie powtórzeń w moim kodzie było nierozsądne – w jaskrawym kontraście do mojej postawy w poprzedniej anegdocie. Powodem jest to, że kontekst jest wszystkim: badałem nową technologię w małym projekcie zabawowym, a nie na dłuższą metę. Kilka dodatkowych linijek i powtórzeń nikomu nie zaszkodziłoby, ale utrata koncentracji zraniła mnie i mój projekt.
Czekaj, więc szukanie najlepszych praktyk może być złym nawykiem? Czasem. Jeśli moim głównym celem byłaby nauka nowego silnika lub nauka w ogóle, byłby to dobrze spędzony czas: majsterkowanie, znajdowanie ograniczeń, odkrywanie niepowiązanych funkcji i niedogodności poprzez badania. Ale zapomniałem, że to nie był mój główny cel i to mnie kosztowało.
To sztuka, jak powiedziałem. A rozwój tej sztuki czerpie korzyści z przypomnienia: Nie rób tego . Przynajmniej skłania cię do zastanowienia się, które wartości grają podczas pracy, a które są dla ciebie najważniejsze w twoim kontekście.
A co z tą drugą zasadą? Kiedy faktycznie możemy zoptymalizować?
2. Nie rób tego jeszcze : ktoś już to zrobił
OK, czy przez Ciebie, czy przez kogoś innego, Twoja architektura została już ustawiona, przepływy danych zostały przemyślane i udokumentowane, i nadszedł czas na kodowanie.
Zróbmy to Nie rób tego jeszcze o krok dalej: nawet nie koduj jeszcze .
To samo może pachnieć jak przedwczesna optymalizacja, ale jest to ważny wyjątek. Czemu? Aby uniknąć przerażającego NIHS, czyli syndromu „nie wynaleziono tutaj” — zakładając, że twoje priorytety obejmują wydajność kodu i minimalizację czasu programowania. Jeśli nie, jeśli Twoje cele są całkowicie zorientowane na naukę, możesz pominąć tę następną sekcję.
Chociaż możliwe jest, że ludzie wymyślają na nowo kwadratowe koło z czystej pychy, wierzę, że uczciwi, skromni ludzie, tacy jak ty i ja, mogą popełnić ten błąd tylko dlatego, że nie znają wszystkich dostępnych nam opcji. Znajomość każdej opcji każdego interfejsu API i narzędzia w twoim stosie oraz utrzymywanie ich na bieżąco w miarę ich rozwoju i ewolucji to z pewnością dużo pracy.
Ale umieszczenie tego czasu sprawia, że stajesz się ekspertem i nie pozwala ci być milionową osobą na CodeSOD, która może zostać przeklęta i wyszydzana za ślad dewastacji pozostawiony przez ich fascynujące podejście do kalkulatorów daty i czasu lub manipulatorów strunowych.
(Dobrym kontrapunktem dla tego ogólnego wzorca jest stary interfejs API Calendar Java, ale został on już naprawiony).
Sprawdź swoją standardową bibliotekę, sprawdź ekosystem swojego frameworka, sprawdź FOSS, który już rozwiązuje Twój problem
Są szanse, że koncepcje, z którymi masz do czynienia, mają dość standardowe i dobrze znane nazwy, więc szybkie wyszukiwanie w Internecie zaoszczędzi mnóstwo czasu.
Jako przykład ostatnio przygotowywałem się do analizy strategii AI dla gry planszowej. Obudziłem się pewnego ranka, zdając sobie sprawę, że analizę, którą planowałem, można przeprowadzić o rzędy wielkości bardziej efektywnie, jeśli po prostu użyję pewnej koncepcji kombinatorycznej, którą pamiętam. W tym czasie nie byłem zainteresowany wymyślaniem algorytmu dla tej koncepcji, byłem już na czele, znając właściwą nazwę do wyszukania. Jednak stwierdziłem, że po około 50 minutach poszukiwań i wypróbowania wstępnego kodu nie udało mi się przekształcić w połowie ukończonego pseudokodu, który znalazłem w poprawną implementację. (Czy możesz uwierzyć, że istnieje post na blogu, w którym autor zakłada nieprawidłowe wyniki algorytmu, implementuje algorytm niewłaściwie, aby pasował do założeń, komentatorzy zwracają na to uwagę, a po latach nadal nie jest to naprawione?) W tym momencie moja poranna herbata kopnąłem i szukałem [name of concept] [my programming language] . 30 sekund później miałem poprawny kod z GitHub i przechodziłem do tego, co naprawdę chciałem robić. Samo doprecyzowanie i uwzględnienie języka, zamiast zakładania, że będę musiał sam go zaimplementować, oznaczało wszystko.

Czas zaprojektować strukturę danych i wdrożyć swój algorytm
…znowu nie graj w code golfa. Nadaj priorytet poprawności i przejrzystości w rzeczywistych projektach.
OK, więc sprawdziłeś i nie ma już nic, co rozwiązuje Twój problem, wbudowanego w Twój łańcuch narzędzi lub swobodnie licencjonowanego w Internecie. Wprowadzasz własne.
Nie ma problemu. Porada jest prosta, w następującej kolejności:
- Zaprojektuj to tak, aby było to łatwe do wytłumaczenia początkującemu programiście.
- Napisz test, który odpowiada oczekiwaniom wynikającym z tego projektu.
- Napisz swój kod, aby początkujący programista mógł z łatwością wyciągnąć z niego projekt.
Proste, ale być może trudne do naśladowania. To tutaj w grę wchodzą nawyki kodowania i zapachy kodu, sztuka, rzemiosło i elegancja. Jest oczywiście aspekt inżynieryjny w tym, co robisz w tym momencie, ale znowu nie graj w golfa kodowego. Nadaj priorytet poprawności i przejrzystości w rzeczywistych projektach.
Jeśli lubisz filmy, oto ktoś, kto wykona mniej więcej powyższe kroki. W przypadku niechęci do wideo podsumuję: jest to test kodowania algorytmu podczas rozmowy o pracę w Google. Respondent najpierw projektuje algorytm w sposób łatwy do komunikowania. Przed napisaniem jakiegokolwiek kodu, są przykłady wyników oczekiwanych przez działający projekt. Wtedy kod w naturalny sposób następuje.
Jeśli chodzi o same testy, wiem, że w niektórych kręgach rozwój sterowany testami może być kontrowersyjny. Myślę, że jednym z powodów jest to, że można to przesadzić, dążyć religijnie aż do poświęcenia czasu na rozwój. (Znowu strzelając sobie w stopę, próbując od samego początku zoptymalizować nawet jedną zmienną za bardzo.) Nawet Kent Beck nie doprowadza TDD do takiej skrajności, wymyślił ekstremalne programowanie i napisał książkę o TDD. Zacznij więc od czegoś prostego, aby upewnić się, że wynik jest poprawny. W końcu i tak robiłbyś to ręcznie po kodowaniu, prawda? (Przepraszam, jeśli jesteś takim programistą rockstar, że nawet nie uruchamiasz swojego kodu po pierwszym napisaniu go. W takim przypadku może rozważysz pozostawienie testu przyszłym opiekunom kodu, aby wiedzieć, że nie zepsuć swoją niesamowitą implementację.) Więc zamiast ręcznie, wizualnej różnicy, z testem na miejscu, już pozwalasz komputerowi zrobić to za ciebie.
Podczas raczej mechanicznego procesu implementacji algorytmów i struktur danych unikaj optymalizacji wiersz po wierszu i nawet nie myśl o używaniu niestandardowego zewnętrznego języka niższego poziomu (montaż, jeśli kodujesz w C, C, jeśli kodujesz w Perlu itp.) w tym momencie. Powód jest prosty: jeśli twój algorytm zostanie całkowicie zastąpiony — i dopiero później dowiesz się, czy jest to potrzebne — wtedy twoje działania optymalizacyjne na niskim poziomie ostatecznie nie przyniosą efektu.
Przykład ECMAScript
Na doskonałej witrynie exercism.io, poświęconej przeglądowi kodu społeczności, niedawno znalazłem ćwiczenie, które wyraźnie sugerowało, aby spróbować optymalizacji pod kątem deduplikacji lub przejrzystości. Zoptymalizowałem pod kątem deduplikacji, aby pokazać, jak absurdalne rzeczy mogą się stać, jeśli za daleko posuniesz się DRY – skądinąd korzystny sposób na kodowanie, jak wspomniałem powyżej. Oto jak wyglądał mój kod:
const zeroPhrase = "No more"; const wallPhrase = " on the wall"; const standardizeNumber = number => { if (number === 0) { return zeroPhrase; } return '' + number; } const bottlePhrase = number => { const possibleS = (number === 1) ? '' : 's'; return standardizeNumber(number) + " bottle" + possibleS + " of beer"; } export default class Beer { static verse(number) { const nextNumber = (number === 0) ? 99 : (number - 1); const thisBottlePhrase = bottlePhrase(number); const nextBottlePhrase = bottlePhrase(nextNumber); let phrase = thisBottlePhrase + wallPhrase + ", " + thisBottlePhrase.toLowerCase() + ".\n"; if (number === 0) { phrase += "Go to the store and buy some more"; } else { const bottleReference = (number === 1) ? "it" : "one"; phrase += "Take " + bottleReference + " down and pass it around"; } return phrase + ", " + nextBottlePhrase.toLowerCase() + wallPhrase + ".\n"; } static sing(start = 99, end = 0) { return Array.from(Array(start - end + 1).keys()).map(offset => { return this.verse(start - offset); }).join('\n'); } } Prawie nie ma tam żadnego powielania strun! Pisząc to w ten sposób, ręcznie zaimplementowałem formę kompresji tekstu dla piwnej piosenki (ale tylko dla piwnej piosenki). Jaka dokładnie była korzyść? Powiedzmy, że chcesz śpiewać o piciu piwa z puszek zamiast z butelek. Mógłbym to osiągnąć, zmieniając jedną instancję bottle na can .
Miły!
…prawidłowy?
Nie, bo wtedy wszystkie testy się psują. OK, łatwo to naprawić: po prostu wyszukamy i wymienimy bottle w specyfikacji testu jednostkowego. I jest to dokładnie tak proste, jak zrobienie tego z samym kodem i niesie to samo ryzyko niezamierzonego złamania rzeczy.
Tymczasem moje zmienne będą później dziwnie nazwane, a takie rzeczy jak bottlePhrase nie będą miały nic wspólnego z butelkami . Jedynym sposobem, aby tego uniknąć, jest przewidzenie dokładnie rodzaju zmiany, która zostanie dokonana i użycie bardziej ogólnego terminu, takiego jak vessel lub container zamiast bottle w moich zmiennych nazwach.
Mądrość zabezpieczania na przyszłość w ten sposób jest dość wątpliwa. Jakie są szanse, że w ogóle będziesz chciał coś zmienić? A jeśli to zrobisz, czy to, co zmienisz, wyjdzie tak wygodnie? W przykładzie bottlePhrase , co zrobić, jeśli chcesz zlokalizować język, który ma więcej niż dwie liczby w liczbie mnogiej? Zgadza się, czas refaktoryzacji, a kod może później wyglądać jeszcze gorzej.
Ale kiedy Twoje wymagania się zmienią, a Ty nie próbujesz tylko je przewidzieć, być może nadszedł czas na refaktoryzację. A może nadal możesz to odłożyć: Ile typów statków lub lokalizacji dodasz, realistycznie? W każdym razie, kiedy musisz zbalansować swoją deduplikację klarownością, warto obejrzeć pokaz Katriny Owen.
Wracając do mojego brzydkiego przykładu: Nie trzeba dodawać, że korzyści płynące z deduplikacji nie są tutaj aż tak bardzo widoczne. Tymczasem ile to kosztowało?
Poza tym, że pisanie zajmuje więcej czasu, teraz jest trochę mniej trywialne do czytania, debugowania i konserwacji. Wyobraź sobie poziom czytelności z dozwoloną umiarkowaną ilością powielania. Na przykład opisując każdą z czterech odmian wersetu.
Ale wciąż nie zoptymalizowaliśmy!
Teraz, gdy twój algorytm został zaimplementowany i udowodniłeś, że jego wyniki są poprawne, gratulacje! Masz linię bazową!
Wreszcie nadszedł czas na… optymalizację, prawda? Nie, nadal Nie rób tego jeszcze . Nadszedł czas, aby wziąć swój punkt odniesienia i zrobić dobry test porównawczy . Ustaw próg dla swoich oczekiwań i umieść go w swoim zestawie testowym. Wtedy, jeśli coś nagle spowalnia ten kod — nawet jeśli nadal działa — będziesz wiedział, zanim wyjdzie za drzwi.
Wciąż wstrzymaj się z optymalizacją, dopóki nie zaimplementujesz całego odpowiedniego doświadczenia użytkownika. Do tego momentu możesz kierować się na zupełnie inną część kodu niż potrzebujesz.
Idź dokończyć aplikację (lub komponent), jeśli jeszcze tego nie zrobiłeś, ustawiając wszystkie bazowe testy porównawcze algorytmu.
Gdy już to zrobisz, jest to świetny moment na utworzenie i przeprowadzenie testów end-to-end obejmujących najczęstsze scenariusze rzeczywistego użytkowania Twojego systemu.
Może przekonasz się, że wszystko jest w porządku.
A może ustaliłeś, że w swoim prawdziwym kontekście coś jest zbyt wolne lub zajmuje zbyt dużo pamięci.
OK, teraz możesz zoptymalizować
Jest tylko jeden sposób, by być w tym obiektywnym. Czas wypuścić wykresy płomieni i inne narzędzia do profilowania. Doświadczeni inżynierowie mogą, ale nie muszą, zgadywać lepiej częściej niż nowicjusze, ale nie o to chodzi: jedynym sposobem, aby wiedzieć na pewno, jest profilowanie. Jest to zawsze pierwsza rzecz do zrobienia w procesie optymalizacji kodu pod kątem wydajności.
Możesz profilować podczas danego testu end-to-end, aby uzyskać to, co naprawdę wywrze największy wpływ. (A później, po wdrożeniu, monitorowanie wzorców użytkowania to świetny sposób, aby być na bieżąco z tym, które aspekty systemu są najistotniejsze do pomiaru w przyszłości).
Zauważ, że nie próbujesz używać profilera w pełni — szukasz bardziej profilowania na poziomie funkcji niż profilowania na poziomie instrukcji, ogólnie, ponieważ Twoim celem w tym momencie jest tylko ustalenie, który algorytm jest wąskim gardłem .
Teraz, gdy użyłeś profilowania do zidentyfikowania wąskiego gardła systemu, możesz teraz podjąć próbę optymalizacji, mając pewność, że warto przeprowadzić optymalizację. Możesz także udowodnić, jak skuteczna (lub nieskuteczna) była twoja próba, dzięki tym bazowym testom porównawczym, które zrobiłeś po drodze.
Ogólne techniki
Po pierwsze, pamiętaj, aby jak najdłużej pozostać na wysokim poziomie:
Czy wiedziałeś? Ostateczna uniwersalna sztuczka optymalizacyjna ma zastosowanie we wszystkich przypadkach:
— Lars Doucet (@larsiusprime) 30 marca 2017 r.
- Rysuj mniej rzeczy
- Zaktualizuj mniej rzeczy
Na poziomie całego algorytmu jedną techniką jest redukcja siły. W przypadku sprowadzania pętli do formuł, pamiętaj jednak o zostawianiu komentarzy. Nie każdy zna lub pamięta każdą formułę kombinatoryczną. Bądź też ostrożny w używaniu matematyki: Czasami to, co uważasz, że może być redukcją siły, ostatecznie nie jest. Załóżmy na przykład, że x * (y + z) ma jasne znaczenie algorytmiczne. Jeśli twój mózg został w pewnym momencie wytrenowany, z jakiegokolwiek powodu, aby automatycznie rozgrupowywać podobne terminy, możesz ulec pokusie przepisania tego jako x * y + x * z . Po pierwsze, tworzy to barierę między czytelnikiem a jasnym znaczeniem algorytmicznym, które tam było. (Co gorsza, teraz jest mniej wydajny ze względu na wymaganą dodatkową operację mnożenia. To tak, jakby rozwijanie pętli po prostu zatkało jej spodnie). własny błąd, zanim go popełnisz.
Niezależnie od tego, czy używasz formuły, czy po prostu zastępujesz algorytm oparty na pętli innym algorytmem opartym na pętli, możesz zmierzyć różnicę.
Ale może możesz uzyskać lepszą wydajność po prostu zmieniając strukturę danych. Zapoznaj się z różnicą wydajności między różnymi operacjami, które musisz wykonać na używanej strukturze, oraz na wszelkich alternatywach. Może hash wygląda trochę bardziej nieporządnie w kontekście, ale czy warto poświęcić więcej czasu na wyszukiwanie w tablicy? To są rodzaje kompromisów, o których decydujesz.
Możesz zauważyć, że sprowadza się to do wiedzy, które algorytmy są wykonywane w Twoim imieniu, gdy wywołujesz funkcję wygody. Więc ostatecznie to to samo, co redukcja siły. A wiedza o tym, co robią biblioteki twojego dostawcy za kulisami, ma kluczowe znaczenie nie tylko dla wydajności, ale także dla uniknięcia niezamierzonych błędów.
Mikrooptymalizacje
OK, funkcjonalność twojego systemu jest zakończona, ale z punktu widzenia UX wydajność można dostroić nieco dalej. Zakładając, że zrobiłeś wszystko, co możliwe, nadszedł czas, aby rozważyć optymalizacje, których dotychczas unikaliśmy przez cały czas. Zastanów się, ponieważ ten poziom optymalizacji jest nadal kompromisem w stosunku do przejrzystości i łatwości konserwacji. Ale zdecydowałeś, że nadszedł czas, więc przejdź do profilowania na poziomie instrukcji, teraz, gdy jesteś w kontekście całego systemu, gdzie ma to znaczenie.
Podobnie jak w przypadku bibliotek, z których korzystasz, niezliczone godziny pracy inżynierskiej zostały włożone dla Twojej korzyści na poziomie kompilatora lub interpretera. (W końcu optymalizacja kompilatora i generowanie kodu to same w sobie ogromne tematy). Dzieje się tak nawet na poziomie procesora. Próba optymalizacji kodu bez świadomości tego, co dzieje się na najniższych poziomach, jest jak myślenie, że posiadanie napędu na cztery koła oznacza, że Twój pojazd może również łatwiej się zatrzymać.
Poza tym trudno jest udzielić dobrych ogólnych porad, ponieważ tak naprawdę zależy to od twojego stosu technologii i tego, na co wskazuje twój profiler. Ale ponieważ mierzysz, jesteś już w doskonałej sytuacji, aby poprosić o pomoc, jeśli rozwiązania nie przedstawiają się organicznie i intuicyjnie z kontekstu problemu. (Sen i czas spędzony na myśleniu o czymś innym również mogą pomóc.)
W tym momencie, w zależności od kontekstu i wymagań dotyczących skalowania, Jeff Atwood prawdopodobnie zasugerowałby po prostu dodanie sprzętu, co może być tańsze niż czas programisty.
Może nie idziesz tą drogą. W takim przypadku może pomóc zbadanie różnych kategorii technik optymalizacji kodu:
- Buforowanie
- Hacki bitowe i te specyficzne dla środowisk 64-bitowych
- Optymalizacja pętli
- Optymalizacja hierarchii pamięci
Dokładniej:
- Wskazówki dotyczące optymalizacji kodu w C i C++
- Wskazówki dotyczące optymalizacji kodu w Javie
- Optymalizacja wykorzystania procesora w .NET
- Buforowanie farmy sieci Web ASP.NET
- Dostrajanie bazy danych SQL lub w szczególności dostrajanie Microsoft SQL Server
- Skalowanie gry Scali! struktura
- Zaawansowana optymalizacja wydajności WordPress
- Optymalizacja kodu za pomocą prototypów JavaScript i łańcuchów zasięgu
- Optymalizacja wydajności React
- Wydajność animacji na iOS
- Wskazówki dotyczące wydajności Androida
W każdym razie mam dla ciebie więcej zakazów :
Nie używaj ponownie zmiennej do wielu różnych celów. Pod względem łatwości konserwacji jest to jak jazda samochodem bez oleju. Tylko w najbardziej ekstremalnych sytuacjach osadzonych miało to sens, a nawet w tych przypadkach twierdzę, że już nie ma. To jest zadanie kompilatora do zorganizowania. Zrób to sam, a następnie przesuń jeden wiersz kodu i wprowadziłeś błąd. Czy iluzja ratowania pamięci jest dla ciebie tego warta?
Nie używaj makr i funkcji wbudowanych, nie wiedząc dlaczego. Tak, obciążenie wywołania funkcji to koszt. Ale unikanie tego często utrudnia debugowanie kodu, a czasami faktycznie spowalnia go. Używanie tej techniki wszędzie tylko dlatego, że jest to dobry pomysł od czasu do czasu, jest przykładem złotego młotka.
Nie rozwijaj ręcznie pętli. Ponownie, ta forma optymalizacji pętli jest prawie zawsze lepiej zoptymalizowana przez zautomatyzowany proces, taki jak kompilacja, a nie przez poświęcanie czytelności kodu.
Ironia w dwóch ostatnich przykładach optymalizacji kodu polega na tym, że w rzeczywistości mogą one być nieskuteczne. Oczywiście, ponieważ robisz testy porównawcze, możesz to udowodnić lub obalić dla konkretnego kodu. Ale nawet jeśli zauważysz poprawę wydajności, wróć do strony graficznej i sprawdź, czy zysk jest wart utraty czytelności i łatwości konserwacji.
To Twoje: Optymalnie Zoptymalizowana Optymalizacja
Próba optymalizacji wydajności może być korzystna. Najczęściej jednak robi się to bardzo przedwcześnie, niesie ze sobą całą litanię złych skutków ubocznych, a co najgorsze, prowadzi do gorszych wyników. Mam nadzieję, że wyszedłeś z rozszerzonym uznaniem dla sztuki i nauki optymalizacji oraz, co najważniejsze, właściwego kontekstu.
Cieszę się, że to pomoże nam odrzucić pomysł pisania doskonałego kodu od samego początku i zamiast tego pisać poprawny kod. Musimy pamiętać, aby optymalizować od góry do dołu, udowodnić, gdzie leżą wąskie gardła i dokonać pomiaru przed i po ich naprawieniu. To optymalna, optymalna strategia optymalizacji optymalizacji. Powodzenia.
