Przedwczesna optymalizacja, czyli jak uniknąć szkodliwego idealizmu

Powinniśmy zapomnieć o małych optymalizacjach, powiedzmy przez 97% czasu: przedwczesna optymalizacja jest źródłem wszelkiego zła.

Donald Knuth

Dlaczego jeden z ojców współczesnej informatyki tak mocno wypowiadał się w tym temacie? Przez dłuższy czas nie brałem tego do siebie. Dopiero po długich latach patrzenia na różnorakie systemy IT zrozumiałem, co ten cytat znaczy. Postaram się przybliżyć temat w możliwie zwięzły i zrozumiały sposób.

Czym jest przedwczesna optymalizacja?

W dużym skrócie jest to praktyka polegającą na mikro-optymalizowaniu podczas początkowych faz rozwoju kodu. Najlepszym sposobem na pokazanie, jak to wygląda jest realny przykład.

Załóżmy, że naszym zadaniem jest podzielenie tekstu, w którym znak minusa jest separatorem fragmentów. Zadanie zdaje się być proste… jednakże może być zrealizowane na wiele radykalnie różnych sposobów. Porównajmy dwie możliwe implementację:

Implementacja nr 1

Kod znaleziony na stackoverflow:

public static List<String> split(String str, char c){
    List<String> list = new ArrayList<>();
    StringBuilder sb = new StringBuilder();

    for (int i = 0; i < str.length(); i++){
        if(str.charAt(i) != c){
            sb.append(str.charAt(i));
        }
        else{
            if(sb.length() > 0){
                list.add(sb.toString());
                sb = new StringBuilder();
            }
        }
    }

    if(sb.length() >0){
        list.add(sb.toString());
    }
    return list;
}

Z pozoru jest to bardzo fajna implementacja. Nie widać w niej oczywistych problemów i jest niesamowicie szybka.

Implementacja nr 2

Ten kawałek też jest ze stackoverflow:

String[] parts = string.split("-");

Widzimy tu coś zupełnie innego. Pojawia się jedna linijka, która załatwia wszystko ale jest też rzędy wielkości wolniejsza od implementacji nr 1.

Która z zamieszczonych implementacji jest “lepsza”?

Patrząc na wydajność pamięciową i obliczeniową nr 1 nie ma konkurencji, a robi dokładnie to samo. Wniosek wydaje się oczywisty. Nr 1 jest bezsprzecznie “lepszą” implementacją. Czy w tym momencie temat jest zamknięty? Jak zapewne domyślacie się, nie jest to takie proste…

Sedno problemu przedwczesnej optymalizacji

Patrząc na obie implementacje mój wybór jest oczywisty – na początku zawsze wybrałbym implementację 2. Głównym powodem mojego wyboru jest prostota (zachęcam do lektury postu na ten temat – KISS ).

W skrócie – 1 nie ma sensu na początkowym etapie rozwoju. Wprowadza dodatkowe skomplikowanie, zwiększa czas dewelopmentu i testowania. Może wprowadzić też dodatkowe błędy, na które ktoś w przyszłości zmarnuje czas. Nie wiemy także, czy w ogóle szybkość w tym miejscu jest potrzebna.

Czy w takim razie powinniśmy programować bez patrzenia na wydajność?

Odpowiedź jest prosta – NIE!

Przedwczesna optymalizacja dotyczy zmian wydajnościowych na początkowym etapie, bez analizy czy są potrzebne. Nie może być to pod żadnym pozorem wymówką do przepychania kodu, który zawiera niewydajne rozwiązania. Dlatego też np. zastosowanie złego algorytmu, który jest wolny w danej sytuacji jest tak samo nieakceptowalne.

Czyli jak należy optymalizować?

W zasadzi, w każdym przypadku można zastosować następujący schemat:

  1. robimy pierwszą implementację, która jest najprostsza, używając do tego najlepiej sprawdzonych i popularnych narzędzi,
  2. podczas testów badamy wydajność używając reprezentatywnych przypadków użycia,
  3. identyfikujemy tzw. “bottleneck”, czyli funkcję w kodzie, która spowalnia najbardziej,
  4. optymalizujemy kłopotliwą funkcję, zwykle drobna zmiana wystarcza, nie popadamy od razu najbardziej skomplikowane zmiany,
  5. powtarzamy 2,3,4 do osiągnięcia należytej wydajności.

Jak to wygląda w praktyce…

Zwykle problemy wydajnościowe dotyczą kilku krytycznych funkcji. Kod mający znaczenie wydajnościowe to w praktyce 1-5% całego kodu. Znacznie lepiej wyjdziemy celując dokładnie w te części, niż wykonując optymalizacje wszędzie.

Co ciekawe, w większości przypadków najwięcej bólu głowy sprawiają operacje odczytujące duże ilości danych w systemie, np. ekrany użytkownika, czy raporty. Mogą one być wywoływane w dużej ilości i wyciągać masę danych. Są to idealne cele do optymalizacji.

W przypadku operacji zmieniających stan danych zwykle mamy kilka najważniejszych, powtarzanych non-stop funkcji. Reszta może być spokojnie zignorowana jeśli chodzi o przyśpieszanie kodu.

Jaki z tego morał?

Przede wszystkim – powstrzymajmy ciężką do odparcia chęć napisania “idealnego” kodu. Nie poprawiajmy rzeczy, które działają dobrze, dopóki w rzeczywistości jest to niezbędne. Nie popisujmy się “super-szybkimi” implementacjami, stosującymi kosmiczne algorytmy. Uwierzcie mi – koledzy z teamu podziękują wam za to…

Na koniec – zagrożenia związane z nadinterpretacją powyższych zasad

Zagadnienie przedwczesnej optymalizacji doczekało się wielu kontrowersji na przestrzeni czasu. Wielu programistów stosuję tą metodę w bardzo brutalny sposób, w rzeczywistości przeinaczając jej sens. Kilka przykładów takich zjawisk:

Stosowanie zasady unikania przedwczesnej optymalizacji, jako zasłona dla błędnego/leniwego kodu

Zasady będące tematem tego postu nigdy nie mogą być traktowane jako usprawiedliwienie błędów programistycznych. Jest różnica pomiędzy zastosowaniem rozwiązania złego, które jest tragiczne wydajnościowo, a optymalizacją dobrego rozwiązania. Pierwsze powinno być traktowane jak każdy inny błąd.

Przeinaczenie w zakresie architektury apliakcji

Jeśli chodzi o architekturę aplikacji – tutaj nie ma miejsca na kompromisy. Systemy muszą być projektowane z wydajnością na myśli. Niedopuszczalna jest niedbałość w strukturze systemu, którą tłumaczymy tym, że “poprawimy wydajność później”. Źle zaprojektowana aplikacja jest bardzo trudna do poprawienia później.

Bezgraniczna wiara w prawo Moora

Chodzi tu o komiczne założenie, że wzrost wydajności jest tak duży, że przegoni ewentualne błędy wydajnościowe. Niestety tak nie jest. Jeśli aplikacja jest niewydajna, to nawet mały wzrost użycia systemu ujawni potężne problemy. Żaden nowy procesor nie będzie w stanie tego nadrobić.

Leave A Comment