Programujesz? Zadbaj o czytelność kodu
Zastanawiałeś się kiedyś czemu tak wiele osób mówi, że czytelność kodu jest ważna? Z pewnością priorytetem jest jego efektywność i możliwie najmniejsza długość, ponieważ krótszy kod oznacza mniej instrukcji, a więc krótszy czas wykonywania programu. Wspólnie z Maciejem Sochą, Senior Software Developerem z firmy Ericsson obalamy kilka mitów dotyczących czytelnego kodu.
11.05.2021 08:40
Mit: Albo wydajność, albo czytelność
Jeden z największych mitów mówi, ż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. Oczywiście istnieją 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. 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.
Eksperci firmy Ericsson o tym wiedzą. Firma 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.
W Polsce również działa drugie pod względem wielkości centrum badań i rozwoju firmy Ericsson na świecie, które w Krakowie i Łodzi zatrudnia około 1800 osób. Efekty pracy polskich inżynierów wykorzystują operatorzy telekomunikacyjni w wielu krajach, w Polsce są nimi Play i Polkomtel.
Przeczytaj również: aktualne oferty pracy w Ericsson Polska
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 całkiem dobrze radzą sobie z tym zadaniem. 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
Posłużmy się prostym przykładem odwijania pętli. Ktoś może mieć pokusę, by ręcznie odwinąć często wykonywaną pętlę, którą wykonuje się stałą liczbę iteracji (albo małą liczbę różnych iteracji), by uniknąć 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? Nawet jak samemu się na takie „dzieło” popatrzy po kilku tygodniach, jednym z pierwszych pytań będzie:
- Po co ja to zrobiłem?
Jeżeli jedna linia będzie musiała być zmieniona, to czy będzie konieczność skopiowania tego 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.
Jednak pomimo tego 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.
Przeczytaj również: Jak pracujemy w Ericsson
Oczywiście 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.