Optymalizacja kodu
- Definicja i właściwości
- rodzaje i poziomy optymalizacji
- co optymalizować
- dobre i złe wyniki optymalizacji
- wnioski
- PVS-Studio
spis treści
Definicja i właściwości
Optymalizacja kodu to każda metoda modyfikacji kodu w celu poprawy jakości i wydajności kodu. Program może być zoptymalizowany tak, że staje się mniejszy rozmiar, zużywa mniej pamięci, wykonuje szybciej lub wykonuje mniej operacji wejścia/wyjścia.
podstawowe metody optymalizacji wymagań powinny być zgodne, jest to, że zoptymalizowany program musi mieć takie same wyniki i skutki uboczne jak jego nieoptymalizowana wersja. Wymóg ten może być jednak zignorowany w przypadku, gdy korzyści z optymalizacji są szacowane na ważniejsze niż prawdopodobne konsekwencje zmiany zachowania programu.
typy i poziomy optymalizacji
optymalizacja może być wykonywana przez automatyczne optymalizatory lub programistów. Optymalizator jest wyspecjalizowanym narzędziem programowym lub wbudowaną jednostką kompilatora (tzw. kompilator optymalizujący). Nowoczesne procesory mogą również zoptymalizować kolejność wykonywania instrukcji kodu.
optymalizacje są podzielone na optymalizacje wysokiego i niskiego poziomu. Optymalizacje wysokiego poziomu są zwykle wykonywane przez programistę, który obsługuje abstrakcyjne elementy (Funkcje, procedury, klasy itp.) i pamiętać o ogólnych ramach zadania optymalizacji projektu systemu. Optymalizacje wykonywane na poziomie elementarnych bloków strukturalnych kodu źródłowego-pętli, odgałęzień itp. – są zwykle określane jako optymalizacje wysokiego poziomu, podczas gdy niektórzy autorzy klasyfikują je do osobnego (“środkowego”) poziomu (N. Wirth?). Optymalizacje niskiego poziomu są wykonywane na etapie, gdy kod źródłowy jest kompilowany do zestawu instrukcji maszynowych i to na tym etapie zwykle stosuje się automatyczną optymalizację. Programiści asemblera wierzą jednak, że żadna maszyna, nawet doskonała, nie jest w stanie zrobić tego lepiej niż wykwalifikowany programista (wszyscy jednak zgadzają się, że biedny programista poradzi sobie znacznie gorzej niż komputer).
co optymalizować
przy ręcznej optymalizacji kodu napotyka się na inny problem: nie wystarczy wiedzieć, jak dokładnie należy przeprowadzić optymalizację, ale także jaka część programu powinna zostać zoptymalizowana. Z różnych powodów (powolne operacje wprowadzania danych, różnica w szybkości pracy operatora ludzkiego i komputera itd.), 90% czasu wykonania programu spędza się wykonując tylko 10% kodu (to stwierdzenie jest raczej spekulacyjne, z zasadą Pareto jako dość wątpliwym gruntem, ale A. Tanenbaum sprawia, że brzmi to przekonująco). Ponieważ optymalizacja zajmuje dodatkowy czas poza czasem poświęconym na rozwój programu, lepiej skoncentruj się na optymalizacji tego krytycznego czasowo 10% kodu, zamiast próbować zoptymalizować cały program. Te fragmenty kodu są znane jako wąskie gardła i mogą być wykrywane przez specjalne narzędzia – profilery – które mogą mierzyć czas potrzebny na wykonanie różnych części programu.
w praktyce jednak optymalizacja jest zwykle wykonywana po etapie “chaotycznego” programowania (w tym takich metod jak “Kopiuj-Wklej”, “zobaczymy później”, “tak jest OK”), a zatem jest mieszaniną optymalizacji jako takiej, refaktoryzacji i poprawek błędów: uproszczenie “queerowych” konstruktów, takich jak strlen(path.c_str ()), warunki logiczne jak (A. x != 0 && a.x != 0) i tak dalej. Profilery niewiele pomagają w tego rodzaju optymalizacji. Niemniej jednak można wykryć te problemy za pomocą narzędzi do analizy statycznej, czyli narzędzi przeznaczonych do wyszukiwania błędów semantycznych, opierając się na głębokiej analizie kodu źródłowego. Jak widać z powyższego przykładu z dziwnym warunkiem, nieefektywny kod może pojawić się w wyniku błędów (jak błąd w naszym przykładzie, gdzie A.x != 0 & & a. y != 0 powinno być zamiast). Potężny analizator statyczny wykryje takie fragmenty kodu i zwróci na nie uwagę, wytwarzając komunikaty ostrzegawcze.
dobre i złe efekty optymalizacji
w programowaniu prawie wszystko powinno być traktowane z punktu widzenia racjonalności – optymalizacja nie jest wyjątkiem. Istnieje przekonanie, że kod napisany przez niedoświadczonego programistę asemblera jest 3-5 razy wolniejszy niż kod generowany przez kompilator (Zubkov). Powszechnie znane jest zdanie Knutha dotyczące wczesnych optymalizacji niskiego poziomu (takich jak próby oszczędzania na operatorach lub zmiennych): “przedwczesna optymalizacja jest korzeniem wszelkiego zła”.
większość programistów nie narzeka na optymalizacje wykonywane przez optymalizator, z których niektóre są konwencjonalne i obowiązkowe. Takie jak np. optymalizacja wywołania ogona w językach funkcyjnych(wywołanie ogona jest szczególnym przypadkiem rekurencji, który może być reprezentowany jako pętla).
jednak należy zrozumieć, że wiele złożonych optymalizacji na poziomie kodu maszynowego może spowodować duże spowolnienie kompilacji. Korzyści, jakie pozwalają uzyskać, mogą być o wiele zbyt nieznaczne w porównaniu z ogólną optymalizacją projektu systemu (Wirth). Należy również pamiętać, że współczesne języki, ze wszystkimi ich składniowymi i semantycznymi “fanaberiami”, mają wiele niuansów i subtelności, aby programista, który nie jest z nimi zaznajomiony, mógł być zaskoczony wynikiem optymalizacji.
na przykład weźmy C++ i tak zwaną optymalizację wartości zwracanej, gdy kompilator unika kopiowania tymczasowego obiektu zwróconego przez funkcję. Ponieważ kompilator pomija kopiowanie, metoda ta jest również nazywana “kopiowaniem”. Tak więc następujący kod:
#include <iostream> struct C { C() {} C(const C&) { std::cout << "A copy was made.\n"; }}; C f() { return C();} int main() { std::cout << "Hello World!\n"; C obj = f();}
może mieć kilka wyjść:
Hello World!A copy was made.A copy was made.Hello World!A copy was made.Hello World!
dziwne, jak się może wydawać, wszystkie trzy wersje są poprawne, ponieważ standard języka pozwala na pominięcie wywołań konstruktora kopiującego w takich przypadkach, nawet jeśli konstruktor ma skutki uboczne (§12.8 Copying Class Objects, paragraf 15).
wniosek
dlatego zawsze powinniśmy rozważyć optymalizację kodu programu przy użyciu specjalistycznych narzędzi, jeśli to możliwe, ale robić to z dużą ostrożnością i być gotowym na prawdopodobieństwo nieoczekiwanych sztuczek ze strony kompilatora.
PVS-Studio
w analizatorze statycznym PVS-Studio zaimplementowany jest zestaw diagnostyki, który umożliwia znalezienie pewnych sytuacji, w których kod może być zoptymalizowany. Jednak PVS-Studio jak każdy inny analizator statyczny nie może służyć jako zamiennik narzędzi do profilowania. Tylko dynamiczne analizatory programów są w stanie zidentyfikować wąskie gardła. Analizatory statyczne nie wiedzą, jakie dane wejściowe otrzymują programy i jak często dany fragment kodu jest wykonywany. Dlatego mówimy, że analizator sugeruje wdrożenie pewnych “mikro-optymalizacji” kodu, które nie gwarantują wzrostu wydajności.
pomimo uznanej wady analizator PVS-Studio działa jako dobre uzupełnienie narzędzi do profilowania. Co więcej, w przypadku ostrzeżeń PVS-Studio, związanych z optymalizacją, kod często staje się prostszy i krótszy. Efekt ten jest rozważany bardziej szczegółowo w artykule “Exploring Microoptimizations Using Tizen Code as an Example”.