12 07 2018 2

Zastanawiałeś się kiedyś czemu tak wiele osób mówi, że czytelność kodu jest ważna? Już nie musisz się głowić, bo w tym artykule obalimy parę mitów. Pewnie, ważniejsza jest jego efektywność i możliwie najmniejsza długość. Krótszy kod oznacza mniej instrukcji, a więc krótszy czas wykonywania programu. Tak więc doświadczony programista powinien zawsze ścisnąć jak najwięcej w jednej linijce. Prawda?

Mit: Albo wydajność, albo czytelność

W zasadzie to nie, wręcz przeciwnie. Jednym z największych mitów jest to, że wydajność i czytelność nie mogą iść w parze, stąd jedno musi zostać poświęcone na rzecz drugiego. Nieprawda. Dobrze przemyślany design, który bierze pod uwagę wydajność, zaimplementowany w prosty sposób, w absolutnej większości przypadków sprawdzi się lepiej, niż źle zaprojektowany, ale „wysoce zoptymalizowany” kod. Zgoda, są odosobnione przypadki, kiedy kod źródłowy musi stać się bardziej skomplikowany, żeby spełnić wymagania wydajnościowe. Jednak to nie reguła, a wyjątek.

Mit: Krócej, znaczy szybciej. Co się dzieje w czasie kompilacji?

Są też inne, bardziej techniczne kwestie - przyjrzyjmy się im. Po pierwsze zobaczmy, co dzieje się w czasie kompilacji. Na początku sprawdzana jest poprawność składniowa programu. Na tym poziomie kompilator nie dba o to, czy ma do czynienia z jedną, tajemniczo wyglądającą linią kodu, czy z kilkoma, które wyglądają znajomo. Tak długo, jak składnia jest poprawna, tak długo wszystko jest w porządku i będzie ona sprawdzana w taki sam sposób.

Sytuacja jest zupełnie inna dla programisty lub - co bardziej prawdopodobne - kilku programistów, którzy będą musieli utrzymywać kod. Prawdopodobnie będą oni czytać go wielokrotnie podczas jego czasu życia. Zrozumienie kilku prostych linijek zajmuje tylko chwilę, a zastanawianie się nad tajemniczym one-linerem przynajmniej minutę czy dwie. Kompilator nie zmęczy się po wielu kompilacjach i będzie wykonywał ten proces ciągle tak samo, nie tracąc przy tym czasu. Z drugiej strony programista zmęczy się, a czasu straci sporo. Cennego czasu, którego już mu nikt nie zwróci.

Ericsson o tym wie. Rozwija ogromny system przy udziale wielu inżynierów oprogramowania, więc kładzie duży nacisk na czytelność kodu oraz stosuje szereg praktyk wspomagających ten cel. Między innymi posiada zestawy zasad pisania kodu (Coding Guidelines) dostosowane do poszczególnych podsystemów, by zachować spójność wizualną (wcięcia, nawiasy itp.) i stylową (nazewnictwo parametrów, funkcji, zmiennych itd.). Ponadto, każdy pisany fragment kodu podlega przeglądowi przez innego programistę zanim zostanie dodany do głównego repozytorium. Ma to też taki plus, że przeważnie ten drugi programista to Twój kumpel siedzący obok, a kumplowi nie robi się na złość pisząc zagmatwany kod, z którym będzie musiał się męczyć przez pół dnia.

Wróćmy do tematu kompilacji. Zakładając, że składnia kodu była poprawna, kompilator przechodzi przez kolejne zadania na drodze do wygenerowania wykonywalnego kodu. W czasie tego procesu jest wiele okazji, by zrobił szereg optymalizacji. Na przykład może on zmienić kolejność wykonywania pewnych wyrażeń, by użyć procesora bardziej efektywnie. Między innymi zlecić wykonanie zadań równolegle, czy też ułatwić przewidywanie rozgałęzień, by uniknąć czyszczenia potoku.

Współczesne kompilatory są w tym całkiem niezłe. Jednak, by nie zepsuć zaprojektowanej funkcjonalności, muszą poprawnie rozpoznać schematyczne części kodu, gdzie takie optymalizacje są bezpieczne. Jeżeli kod zawiera rozpoznawalny schemat, jest bardziej prawdopodobne, że kompilator poprawnie rozpozna okazję do optymalizacji. W wyniku tego wygeneruje bardziej efektywny kod wykonywalny.

Mit: Zawsze dobrze optymalizować samodzielnie. Przykład: odwijanie pętli

Weźmy nawet prosty przykład odwijania pętli. Ktoś może mieć pokusę, by ręcznie odwinąć często wykonywaną pętlę, którą często wykonuje się stałą liczbę iteracji (albo małą liczbę różnych iteracji), by uniknąć - powiedzmy - rozgałęzień, gdy sprawdzany jest warunek pętli. Naturalnie kod stanie się dłuższy, wyrażenia zaczną się powtarzać, a ktoś zacznie się zastanawiać czy wyrażenia są rzeczywiście takie same i jeżeli tak, to dlaczego. Szczerze gwarantuję, że nawet jak samemu się na takie „dzieło” popatrzy po kilku tygodniach, jedną z pierwszych myśli będzie coś w stylu:

Po co ja to zrobiłem?

Jeżeli jedna linia będzie musiała być zmieniona, to czy będzie to trzeba skopiować do innych linii? We wszystkich miejscach, które wydają się różnić jedynie liczbą wyrażeń czy tylko w tym jednym? Pytania będą się mnożyć. Przynajmniej zostawmy koledze i sobie komentarz w kodzie, że to odwinięta pętla.

Jasne, ale nadal niemożliwe będzie wychwycenie jednym spojrzeniem czy niczego nie przegapiliśmy. Tak więc czytelność i łatwość utrzymania cierpi. Kompilator zobaczy kilka długich sekcji kodu, każda na innej gałęzi decyzyjnej, z kilkoma lokalnymi zmiennymi i naprawdę nie będzie wiedział, skąd to wszystko się wzięło. Brak możliwości przewidywania rozgałęzień będzie mniej lub bardziej kosztowny - w zależności od architektury CPU i np. rozmiaru pamięci podręcznej.

OK, można spojrzeć na wygenerowane instrukcje i zaadoptować oryginalny kod źródłowy, by wziąć to pod uwagę. Jednak co, jeżeli kompilator i/lub procesor się zmieni? Wtedy będzie trzeba za każdym razem robić to od początku. To kosztuje czas i - co za tym idzie - pieniądze. Lepiej zostawić pętlę w spokoju i pozwolić odwinąć ją kompilatorowi, gdy uzna to za stosowne. Kompilator zna CPU i np. jego cache instrukcji. Ma lepszą wiedzę i kontrolę nad tym, ile iteracji pętli rozwinąć, by lepiej użyć pamięci podręcznej procesora i zminimalizować koszty nietrafionego przewidywania rozgałęzień. Tak więc czytelność nie ucierpi. Wydajność może być nawet lepsza, a dodatkowo będzie potencjalnie przenośna na inny kompilator czy CPU.

Podsumowując

  • Zawsze włączaj konkretne wymagania wydajnościowe do oryginalnego designu
  • Implementuj w jak najprostszy sposób
  • Pozwól robić kompilatorowi jego optymalizacje
  • Wszelkie ręczne optymalizacje popieraj starannymi pomiarami

 

O autorze

Maciej Socha - Senior Software Developer w zespole 3G - wspiera aktualnie Product Design Troubleshooting. W Ericsson pracuje od ponad sześciu lat, tymczasem w obszarze telekomunikacji już 12. Sam o sobie mówi, że jest praktyczny, robi co potrzeba i gdzie trzeba, jeśli tylko umie, albo... może się nauczyć. Po pracy interesuje się windsurfingiem, podróżami i tuningiem aut, a także symulatorami aut wyścigowych.