Autor: JANUZI
W trakcie programowania jedną z rzeczy, na które należy zwracać uwagę jest wydajność kodu. Bardzo łatwo jest napisać w parę minut program/skrypt, ale nie zawsze będą one działały z pełną wydajnością. Z tego też powodu warto określić miejsca krytyczne, a następnie poprawić je.
Bardzo często przyjmuje się, że 80% czasu działania programu skupia się w 20% kodu. Jeśli uda się zlokalizować ten 20% kawałek, to jest duża szansa na poprawienie ogólnych wyników programu/skryptu.
Ręczne zliczanie
Jeśli nie mamy dostępu do odpowiednich modułów PHP (jak np. xdebug)), musimy poradzić sobie sami. Przede wszystkim potrzebna będzie funkcja microtime() podająca czas z dokładnością do mikrosekund. Wstawiamy jej wywołanie na początku oraz na końcu funkcji tak, aby zapamiętać wartość (czyli czas rozpoczęcia i zakończenia działania). Różnicę pomiędzy wartościami dodajemy do ogólnego wyniku danej funkcji. Dodatkowo zwiększamy o jeden licznik wywołań funkcji. Dzięki temu uzyskamy informacje o liczbie wywołań funkcji oraz o ogólnym czasie potrzebnym na wykonanie tych wywołań (jeśli podzielimy czas przez liczbę otrzymamy średni czas). Jak widać zadanie to wymaga przygotowania tablic pozwalających przechować informacje. Konieczne jest również dodanie global $tablica; do każdej funkcji. W przypadku niewielkiego skryptu rozwiązanie jest akceptowalne. Jeśli natomiast skrypt składa się z kilkunastu/kilkudziesięciu kawałków, to będzie potrzeba sporo czasu, aby zmodyfikować całość.
Automatyczne zliczanie
W przypadku wielu serwerów dostęp do bardziej „egzotycznych” rozszerzeń jest utrudniony. Dlatego też warto jest wypróbować tę metodę w domowym zaciszu na lokalnym serwerze. Jednym z takich rozszerzeń jest Xdebug. Użytkownicy Linuksa są w tym przypadku w lepszej sytuacji. Kompilator mają od razu pod ręką, a niektóre dystrybucje (np. Gentoo) mają już odpowiednią paczkę gotową do ściągnięcia i zainstalowania. W przypadku systemu Windows konieczne będzie wcześniejsze zainstalowanie kompilatora (prawdopodobnie Visual C++ będzie tym właściwym). Przed wykonaniem jakichkolwiek działań warto sprawdzić w phpinfo() czy moduł xdebug pojawił się na liście. Jeśli jest, to przystępujemy do modyfikowania kodu.
Na początku skryptu wstawiamy:
$debug_script = false ; if( $debug_script ) { $xd = extension_loaded('xdebug'); if ($xd) { xdebug_start_profiling(); xdebug_start_trace(); } }
a na końcu:
if( $debug_script ) { if ($xd) xdebug_dump_function_profile(4) ; echo "Użyto : ".xdebug_memory_usage()." pamięci<br />" ; }
Jako parametr funkcji xdebug_dump_function_profile() podaje się liczbę określającą dane, które mają być przedstawione. Może to być liczba wywołań funkcji, czas spędzony w każdej funkcji, ilość zużytej pamięci (o ile PHP jest ustawione z opcją limitowania pamięci), itd.
Kiedy już mamy potrzebne informacje przystępujemy do poprawiania miejsc, które tego wymagają. Co prawda nie da się poprawić funkcji zewnętrznych (jak np. eregi()), ale cała reszta jest do dyspozycji programisty. Jeśli łączymy się z bazą danych, to prawdopodobnie funkcja nawiązująca połączenie będzie zajmowała najwięcej czasu. Jednak należy pamiętać, że na ogólny czas wpływ mają nawet miejsca trwające bardzo krótko.
Jeżeli PHP skompilowano z opcją ograniczenia pamięci, to xdebug pozwoli odczytać ilość pamięci używanej przez skrypt: xdebug_memory_usage(). Jeśli opcja jest niedostępna, to można założyć, że każdy skrypt zajmuje 10 razy tyle ile wynosi rozmiar pliku w którym został umieszczony. Dla pliku o wielkości 500 bajtów potrzebne będzie 5000 bajtów pamięci. Oszacowanie nie będzie zbyt dokładne, ale pozwoli mniej-więcej określić ilość pamięci przypadającej na jednego odwiedzającego. Bardzo łatwo będzie policzyć ile osób na raz może obejrzeć stronę zanim pojawi im się komunikat o braku x bajtów pamięci. Na ilość pamięci wpływ ma wielkość danych umieszczanych w zmiennych, rozmiar tabel, liczba zmiennych itd. W tym przypadku najlepiej pozbywać się większych tablic wtedy, gdy tylko przestaną być potrzebne. Podobnie ze zmiennymi napisowymi zawierającymi bardzo długie łańcuchy.
Mysql
Bardzo często najdłuższą operacją jest łączenie się z samą bazą. Jednakże w przypadku zastosowania wielu zapytań oraz nieoptymalnych zapytań ogólny czas współpracy z bazą może znacznie wzrosnąć. Na ten czas wpływ mają: liczba zapytań, liczba wyników, liczba wybranych kolumn, dostępność indeksów.
Indeksy baz danych
Na początek skupmy się na indeksach. Baza to nic innego jak pliki zawierające dane zapisane jedne za drugimi. Dostęp do określonego zestawu danych uzyskać można przeglądając kolejno te dane, lub poprzez specjalną zmienną określającą miejsce w pliku. Powiedzmy, że mamy tabelę z imionami:
(nr_linii)|id*|imie| 1|11|ala| 2|21|ola| 3|23|beata| 4|34|ania| 5|41|magda|
* – autoincrement, primary key (czyli w uproszczeniu indeks)
Jak widać imiona zostały ponumerowane, numer ten (jeśli jest indeksem) może służyć do określenia wiersza z bazy, który należy odczytać w operacji:
Select imie from tabela where id = $numer
Sprawa komplikuje się w momencie, gdy chcemy znaleźć id powiązane ze wskazanym imieniem. Imiona są zapisane losowo, więc bezpośrednio nie ma możliwości odwołania się do wskazanego wiersza. Tutaj pojawia się pomysł: „A gdyby tak zrobić drugą tablicę, w której imiona będą posortowane kolejno ?”. W tym przypadku wyszukanie nie będzie stanowiło specjalnego problemu. Jeżeli dane potraktujemy jako drzewo binarne, to jesteśmy w stanie w krótkim czasie znaleźć szukany element (za każdym sprawdzeniem odrzucana jest połowa danych). Na podobnej zasadzie działają indeksy w bazie, w uproszczeniu:
ala=>1 ania=>4 beata=>3 magda=>5 ola=>2
Przy wyszukiwaniu id:
select id from tabela where imie = magda
nastąpi odwołanie do kluczy powiązanych z kolumną imie, na tej podstawie określona zostanie linia w tabeli, a tym samym natychmiastowo wartość kolumny id.
Indeksy warto stosować tam, gdzie jest to niezbędne. Potencjalnymi kandydatami są zatem kolumny występujące przy WHERE, LEFT JOIN ON, itd. Pozostałych kolumn nie opłaca się indeksować. Nie da to żadnego zysku, a dodatkowo obniży wydajność całości (przy wstawianiu nowych danych oraz przy uaktualnianiu poprzednich danych indeksy muszą zostać przeliczone) oraz spowoduje powiększenie się tabeli. Informacje o stanie tabeli można podejrzeć w phpmyadminie, będzie tam podany rozmiar danych oraz rozmiar indeksów.
Explain
Przy przeglądaniu phpmyadmina można natknąć się na okienko z nazwą „zapytanie SQL”. W dolnej belce tego okienka umieszczona została opcja „Wyjaśnij SQL”. Opcja ta powoduje wywołanie zapytania do mysql z dodanym słowem EXPLAIN. Słowo to powoduje wyświetlenie wyjaśnienia związanego z zapytaniem, a dokładniej pokazany zostaje tok „logicznego myślenia” bazy. Pokazane są takie informacje jak: liczba sprawdzonych rekordów, dostępne klucze, użyte klucze, sposób połączenia tabel, typ szukania danych w tabeli. Najważniejszymi z nich są liczba rekordów oraz typ szukania danych. Są one ze sobą powiązane. Dla typu wynoszącego ALL liczba rekordów będzie wynosiła tyle ile jest rekordów w tabeli. W przypadku zapytania dotyczącego kilku tabel wyświetlone zostaną informacje powiązane z każdą tabelą. Niezależnie od liczby tabel najważniejszym wyznacznikiem jest iloczyn wierszy. Powiedzmy, że mamy tabele A i B, z których uzyskujemy zestaw wyników za pomocą LEFT JOIN:
id|select_type|table|type|possible_keys|key|key_len|ref|rows|Extra 1|SIMPLE|A|ALL|NULL|NULL|NULL|NULL|1000|using where 2|SIMPLE|B|eq_ref|PRIMARY|PRIMARY|5|----|10| (---- informacje o sposobie powiązanie tabel)
W tym przypadku uzyskujemy wynik równy 10000. Jak widać pierwsza tabela nie ma żadnego dostępnego klucza i konieczne jest przejrzenie wszystkich rekordów. Druga tabela powiązana jest poprzez swój klucz główny, dzięki czemu odwołanie dotyczy jedynie 10 rekordów. Warto przyjrzeć się tabeli A i sprawdzić czy dodanie klucza powiązanego z warunkiem where da pożądany rezultat. Bardzo często to wystarcza, aby zmniejszyć ogólną liczbę wierszy, a tym samym przyspieszyć pokazywanie się strony.
Liczba zapytań
Z liczbą zapytań jest podobnie jak z liczbą połączeń z bazą danych, im mniej, tym lepiej. Każde zapytanie wymaga wstępnej obróbki, tj. sprawdzenia czy jest prawidłowe i czy spełnia wszystkie wymagania. Dlatego też ich liczba powinna być ograniczana do minimum (o ile jest taka możliwość). Niekiedy należy odejść od tej reguły. Wszystko zależy od tego co pokaże Explain. Niekiedy lepiej jest rozbić zapytanie na dwa różne i uzyskać wynik 100, 1000, 10000 razy lepszy.
Liczba wyników i liczba kolumn
Bardzo często dane pobierane są porcjami. Dzięki temu dane wyświetlane na ekranie są czytelniejsze. Nawet tutaj ważne jest odpowiednie działanie. PHP ma funkcję mysql_num_rows(), która aż się prosi o użycie. Jednakże pobieranie wszystkich wierszy z bazy tylko po to, żeby je potem podliczyć mija się z celem. Lepiej od razu poprosić bazę o informacje o liczbie wierszy:
select count(*) from tabela
Po określeniu liczby stron można skonstruować zapytanie stosując LIMIT [przesunięcie,] liczba. Ograniczy to liczbę danych wysyłanych przez bazę.
Drugą ważną sprawą jest liczba kolumn w wyniku. Pobieranie całości za pomocą Select * from tabela nie zawsze jest dobrym sposobem. Zwłaszcza, gdy potrzebne są informacje tylko z jednej kolumny. Im mniej kolumn przesłanych do skryptu, tym szybsze wykonanie całości.
Statyczny kontra dynamiczny
Strony statyczne mają wiele wad, ale mają także zalety. Jedną z zalet jest szybkość ich przetwarzania i wysyłania do przeglądarki. W przypadku stron dynamicznych główną zaletą jest możliwość wpływania na wygląd, poprzez bazę danych czy zawartość pliku tekstowego. Jeśli połączy się ich zalety, to można uzyskać szybsze wyświetlanie się strony oraz wpływ na jej zawartość. Rozwiązanie ma także wady, jak na przykład redundancja danych (to samo w pliku co w bazie). Dobrym rozwiązaniem zatem będzie takie dobranie stron „statycznych”, aby przyspieszyć ogólne działanie serwisu oraz aby nie zapchać dysku. Najlepsze do tego będą te podstrony, które wymagają sporego zaangażowania ze strony bazy danych (lub samego PHP) i które zmieniają się bardzo wolno. Tego ostatniego warunku nie spełnia chociażby strona wyników szukania zadanego słowa (chyba, że na stronie niewiele się dzieje, wtedy można wygenerować kilka najczęstszych wyników).
Do generowania gotowych wyników potrzebne będą funkcje: fopen(), fwrite(), fread(), fclose(), file_exists(), filemtime(), time(), posłużą one kolejno do: otwarcia pliku, zapisania zawartości, odczytania zawartości, zamknięcia pliku, sprawdzenia czy plik istnieje, sprawdzenia kiedy plik był modyfikowany, sprawdzenia aktualnego czasu. Niezbędne także będzie utworzenie katalogu, który przechowywać będzie wszystkie wygenerowane pliki. Katalog ten powinien mieć ustawione prawa zapisu dla każdego. Zastępujemy następnie kod:
pokaż dane z tabeli
kodem:
sprawdź czy plik istnieje oraz czy ostatnia modyfikacja nie była później niż zadany przedział czasu a) powyższy warunek jest spełniony, można odczytać zawartość pliku i pokazać ją b) warunek nie jest spełniony, zapamiętujemy pokazywane dane i zapisujemy je do pliku
Należy jedynie określić co ile sekund ma być na nowo generowany plik zawierający dane. Przykładowo może to być: filemtime( $nazwa_pliku ) + 3600 > time(), czyli godzina.
Należy wspomnieć, że skrypty szablonów, takie jak Smarty, czy Fast.Template mają możliwość zapamiętywania wygenerowanych stron.
Uwagi końcowe
Po wycięciu niepotrzebnych fragmentów kodu, ograniczeniu liczby funkcji do takich, które są niezbędne w danym miejscu, zmniejszeniu liczby zapytań do mysql i zoptymalizowaniu ich, strona zacznie się zdecydowanie szybciej generować.
Przeglądarki mają możliwość zapamiętywania przeglądanych stron. Dzięki temu strona szybciej się pojawia, a dodatkowo łącze nie jest obciążane. Uzyskuje się to poprzez stosowanie dodatkowych nagłówków. Rozwiązanie to jest dobre, gdy konieczne jest wyświetlenie wielu statycznych stron i gdy przewiduje się wielokrotne oglądanie strony przez odwiedzającego.
Administratorem Twoich danych osobowych będzie Rafał Płatek, prowadzący działalność gospodarczą pod firmą CREAM.SOFTWARE RAFAŁ PŁATEK, wpisaną do rejestru ewidencji gospodarczej CEiDG pod numerem NIP 681-112-89-55. Szczegóły związane z przetwarzaniem danych osobowych znajdziesz w polityce prywatności.