Spraw, aby działał, czyść go, uczyń go SOLIDNYM, A NASTĘPNIE spraw, aby działał tak szybko, jak powinien .
To powinna być normalna kolejność rzeczy. Twoim pierwszym priorytetem jest stworzenie czegoś, co przejdzie testy akceptacyjne, które wstrząsną wymaganiami. To jest twój pierwszy priorytet, ponieważ jest to pierwszy priorytet twojego klienta; spełnianie wymagań funkcjonalnych w terminach rozwoju. Kolejnym priorytetem jest napisanie czystego, czytelnego kodu, który jest łatwy do zrozumienia, a zatem może być utrzymywany przez potomność bez żadnych WTF, gdy stanie się to konieczne (prawie nigdy nie jest to kwestia „jeśli”; ty lub ktoś po tobie będzie musiał odejść wróć i zmień / napraw coś). Trzecim priorytetem jest dostosowanie kodu do metodologii SOLID (lub GRASP, jeśli wolisz), która umieszcza kod w modułowych, wielokrotnego użytku, wymiennych częściach, które ponownie pomagają w utrzymaniu (nie tylko mogą zrozumieć, co zrobiłeś i dlaczego, ale są czyste linie, wzdłuż których mogę chirurgicznie usunąć i wymienić fragmenty kodu). Ostatnim priorytetem jest wydajność; jeśli kod jest na tyle ważny, że musi być zgodny ze specyfikacjami wydajności, prawie na pewno jest wystarczająco ważny, aby najpierw był poprawny, czysty i SOLIDNY.
Nawiązując do Christophera (i Donalda Knutha), „przedwczesna optymalizacja jest źródłem wszelkiego zła”. Ponadto rozważane rodzaje optymalizacji są niewielkie (odniesienie do nowego obiektu zostanie utworzone na stosie, niezależnie od tego, czy nadasz mu nazwę w kodzie źródłowym, czy nie), a także typu, który nie może powodować żadnych różnic w kompilacji IL. Nazwy zmiennych nie są przenoszone do IL, więc ponieważ deklarujesz zmienną tuż przed jej pierwszym (i prawdopodobnie jedynym) użyciem, postawiłbym trochę pieniędzy na piwo, że IL jest identyczna między Twoimi dwoma przykładami. Twój współpracownik ma więc 100% racji; patrzysz w niewłaściwym miejscu, jeśli patrzysz na zmienną nazwaną vs instancję wbudowaną, aby coś zoptymalizować.
Mikrooptymalizacje w .NET prawie nigdy nie są tego warte (mówię o 99,99% przypadków). Być może w C / C ++ JEŚLI wiesz, co robisz. Pracując w środowisku .NET, jesteś już wystarczająco daleko od metalu sprzętu, co powoduje znaczne obciążenie podczas wykonywania kodu. Biorąc pod uwagę, że jesteś już w środowisku, które wskazuje, że zrezygnowałeś z błyskawicznej prędkości i zamiast tego chcesz napisać „poprawny” kod, jeśli coś w środowisku .NET naprawdę nie działa wystarczająco szybko, albo jego złożoność jest zbyt wysoko, lub powinieneś rozważyć równoległe ustawienie. Oto kilka podstawowych wskazówek dotyczących optymalizacji; Gwarantuję Ci, że Twoja optymalizacja wydajności (prędkość uzyskana z poświęconego czasu) gwałtownie wzrośnie:
- Zmiana kształtu funkcji ma znaczenie bardziej niż zmiana współczynników - złożoność WRT Big-Oh, można zmniejszyć o połowę liczbę kroków, które należy wykonać w algorytmie N 2 , a mimo to algorytm złożoności kwadratowej jest wykonywany nawet w o połowę mniej niż kiedyś. Jeśli jest to dolna granica złożoności dla tego rodzaju problemu, niech tak będzie, ale jeśli istnieje NlogN, liniowe lub logarytmiczne rozwiązanie tego samego problemu, zyskasz więcej, zmieniając algorytmy w celu zmniejszenia złożoności, niż optymalizując ten, który masz.
- To, że nie widzisz złożoności, nie oznacza, że to Cię nie kosztuje - wiele z najbardziej eleganckich jedno-liniowych słów tego słowa działa strasznie (na przykład sprawdzanie liczb pierwszych Regex jest funkcją złożoności wykładniczej, a jednocześnie wydajną pierwsza ocena polegająca na podzieleniu liczby przez wszystkie liczby pierwsze mniejsze niż pierwiastek kwadratowy jest rzędu O (Nlog (sqrt (N))). Linq to świetna biblioteka, ponieważ upraszcza kod, ale w przeciwieństwie do silnika SQL, .Net kompilator nie będzie próbował znaleźć najbardziej wydajnego sposobu wykonania zapytania. Musisz wiedzieć, co się stanie, gdy użyjesz metody, a zatem dlaczego metoda może być szybsza, jeśli zostanie umieszczona wcześniej (lub później) w łańcuchu, podczas tworzenia te same wyniki.
- OTOH, prawie zawsze istnieje kompromis między złożonością źródła a złożonością środowiska wykonawczego - SelectionSort jest bardzo łatwy do wdrożenia; prawdopodobnie możesz to zrobić w 10LOC lub mniej. MergeSort jest nieco bardziej złożony, Quicksort bardziej, a RadixSort jeszcze bardziej. Jednak wraz ze wzrostem złożoności algorytmów (a tym samym z czasem programowania „z góry”) zmniejsza się złożoność środowiska wykonawczego; MergeSort i QuickSort to NlogN, a RadixSort jest ogólnie uważany za liniowy (technicznie jest to NlogM, gdzie M jest największą liczbą w N).
- Przerwij szybko - jeśli istnieje czek, który można wykonać niedrogo, który najprawdopodobniej będzie prawdziwy i oznacza, że możesz przejść dalej, wykonaj go najpierw. Jeśli na przykład twój algorytm dba tylko o liczby kończące się na 1, 2 lub 3, najbardziej prawdopodobnym przypadkiem (w przypadku danych całkowicie losowych) jest liczba, która kończy się na innej cyfrze, więc sprawdź, czy liczba NIE kończy się na 1, 2 lub 3 przed sprawdzeniem, czy liczba kończy się na 1, 2 lub 3. Jeśli element logiczny wymaga A i B, a P (A) = 0,9, a P (B) = 0,1, to sprawdź Najpierw B, chyba że jeśli! A, to! B (jak
if(myObject != null && myObject.someProperty == 1)
) lub B zajmuje więcej niż 9 razy dłużej niż A do oceny ( if(myObject != null && some10SecondMethodReturningBool())
).
- Nie zadawaj żadnych pytań, na które znasz już odpowiedź - jeśli masz serię warunków awaryjnych, a jeden lub więcej z tych warunków zależy od prostszego warunku, który również należy sprawdzić, nigdy nie sprawdzaj obu te niezależnie. Na przykład, jeśli masz czek, który wymaga A, i czek, który wymaga A i B, powinieneś sprawdzić A, a jeśli to prawda, powinieneś sprawdzić B. Jeśli! A, to! A i& B, więc nawet nie zawracaj sobie tym głowy.
- Im więcej razy coś robisz, tym bardziej powinieneś zwracać uwagę na to, jak to się robi - Jest to wspólny motyw w rozwoju, na wielu poziomach; w ogólnym sensie rozwoju: „jeśli wspólne zadanie jest czasochłonne lub kłopotliwe, rób to, dopóki nie będziesz zarówno sfrustrowany, jak i posiadasz wystarczającą wiedzę, aby znaleźć lepszy sposób”. W kategoriach kodowych, im więcej razy uruchamiany jest nieefektywny algorytm, tym więcej zyskasz na ogólnej wydajności dzięki optymalizacji. Istnieją narzędzia do profilowania, które mogą wziąć zestaw binarny i jego symbole debugowania i pokazać, po przejrzeniu niektórych przypadków użycia, jakie wiersze kodu zostały uruchomione najbardziej. Te linie i linie, które je prowadzą, są tym, na co powinieneś zwrócić największą uwagę, ponieważ każdy wzrost wydajności, który tam osiągniesz, zostanie zwielokrotniony.
- Bardziej złożony algorytm wygląda jak mniej złożony algorytm, jeśli wrzuci się w niego wystarczającą ilość sprzętu . Są chwile, w których musisz sobie uświadomić, że twój algorytm zbliża się do technicznych ograniczeń systemu (lub jego części), na którym go uruchamiasz; od tego momentu, jeśli musi być szybszy, zyskasz więcej, po prostu uruchamiając go na lepszym sprzęcie. Dotyczy to również równoległości; N 2 algorytm -complexity, kiedy uruchamiane na N rdzeni, wygląda liniowych. Jeśli więc masz pewność, że osiągnąłeś niższy poziom złożoności dla rodzaju pisanego algorytmu, poszukaj sposobów na „dzielenie i podbijanie”.
- Jest szybki, gdy jest wystarczająco szybki - o ile nie pakujesz ręcznie zestawu do celowania w konkretny układ, zawsze można coś zyskać. Jednakże, chyba że CHCESZ być ręcznym pakowaniem, zawsze musisz pamiętać, co klient nazwałby „wystarczająco dobrym”. Ponownie „przedwczesna optymalizacja jest źródłem wszelkiego zła”; kiedy twój klient nazywa to wystarczająco szybko, robisz to, dopóki on nie uważa, że to wystarczająco szybko.