Optymalizacja skryptów PHP

Coraz częściej po PHP sięgają osoby, które mają niewielkie doświadczenie programistyczne. Skrypty pisane bez przemyślenia działają wolno i mocno obciążają procesor serwera, na czym cierpią i goście stron i administratorzy serwerów.

Skrypty PHP można przyspieszyć przez stosowanie pewnych zasad przy pisaniu kodu, lecz optymalizację dobrze jest zacząć jeszcze przed rozpoczęciem kodowania. Trzeba się zastanowić, czy potrzebna jest maksymalna optymalizacja kosztem czytelności kodu. Czas wykonywania kodu PHP nie zawierającego zapytań SQL mierzony jest w milisekundach, tak więc nawet 100% zysku to ułamki sekundy. Co z tego, że kod PHP jest idealny, jeśli źle napisane zapytanie SQL będzie wykonywane 10 razy dłużej niż sam kod? Warto jest jednak wyrabiać w sobie dobre nawyki.

Pisanie każdego skryptu powinno się zacząć od chwili zadumy, najlepiej z kartką i ołówkiem. Trzeba poważnie przemyśleć konstrukcję skryptu: dla kogo jest przeznaczony, pod jakim obciążeniem będzie pracował, jakie oprogramowanie dodatkowe będzie dostępne itp. Jeśli skrypt pisany jest dla konkretnego odbiorcy, niezbędne jest uwzględnienie życzeń oraz możliwości serwera zamawiającego. Etap projektowania jest najważniejszy dla końcowego wyglądu kodu.

Jedną z najważniejszych decyzji jest ustalenie, czy skrypt ma działać szybko pod małym obciążeniem, czy może lepiej postawić na niewielkie zmiany wydajności przy rosnącej ilości wywołań na sekundę. Decyzję tą trzeba podjąć po uwzględnieniu grupy docelowej danego skryptu. Trzeba także pomyśleć, który sposób przechowywania danych jest najlepszy. Bazy danych, choć kuszą łatwością przechowywania danych oraz ich wyciągania, czasem mogą być mniej wydajne od bezpośredniego zapisu do plików.

Kiedy aplikacja się rozrasta, zawsze znajdą się partie kodu, których funkcjonalność jest identyczna, a działają tylko na różnych zmiennych. Wtedy warto pomyśleć o stworzeniu funkcji. Ułatwia to późniejsze modyfikacje kodu – wystarczy poprawić go w jednym miejscu zamiast nanosić poprawki w różnych miejscach skryptu.

Przy okazji tworzenia bibliotek funkcji warto jest wspomnieć o instrukcjach include i require, dzięki którym funkcje można przenieść do zewnętrznych plików. Dołączając biblioteki dobrze jest korzystać z instrukcji require_once. Zabezpieczy ona plik z funkcjami przed wielokrotnym dołączeniem.

Pomiar wydajności

Posiadając kilka możliwych rozwiązań, dobrze już we wczesnym stadium opracowywania skryptu porównać ich wydajność i wybrać najlepszy algorytm.

Najprostszym sposobem pomiaru wydajności jest pomiar czasu wykonania, często połączony z wielokrotnymi powtórzeniami. Aby dokonać pomiaru czasu należy odczytać czas na początku i na końcu sprawdzanego kodu. Wynikiem jest różnica czasu końcowy i początkowego. Jednak dokładność sekundowa, którą oferuje funkcja time(), jest za mała. Należy skorzystać z funkcji microtime(). Kod odczytujący czas z dokładnością do mikrosekund wygląda tak:

function getmicrotime()
{
   list($usec, $sec) = explode(" ",microtime());
   return ((float)$usec + (float)$sec);
}

Jednak czasami i ta dokładność jest za mała – w takim wypadku trzeba wstawić kod w pętle i wykonać go wielokrotnie.

Pomiar czasu to najprostsza z metod. Bardziej skomplikowane możliwości oferuje profiler wbudowany w debugger DBG, który można znaleźć pod adresem http://dd.cron.ru/dbg/. Trochę inne podejście oferują programy służące do testowania wydajności serwera Apache. Mianowicie pozwalają na badanie ogólnej wydajności całego skryptu. Program apachebenchmark (ab) dostarczany jest razem ze źródłami serwera, natomiast program flood dostępny jest pod adresem http://httpd.apache.org/test/flood/ . Programy te wielokrotnie, raz po raz łączą się z serwerem WWW pobierając określoną stronę. Na podstawie czasu oczekiwania na przesłanie strony generowana jest statystyka.

Pomiędzy kodem PHP a kodem wykonywanym przez Zend Engine (silnik przetwarzający kod PHP) jest jeszcze postać, która nazywa się opcode. Ten pośredni kod może być optymalizowany lub nawet przechowywany w pamięci podręcznej. Optymalizacja najskuteczniej działa dla kodu w którym jest wiele pętli, lecz w szczególnych przypadkach może doprowadzić do spowolnienia kodu. Listę pakietów optymalizujących kod można znaleźć pod adresem http://php.weblogs.com/php_debugger_cache

Cytowanie

W PHP, do cytowania stringów, można używać znaków ‚ lub „. Nazwy zmiennych objęte w podwójne cudzysłowy są przetwarzane przez PHP i za zmienne podstawione zostają ich wartości. Przez to parser PHP musi przeglądać ciągi tekstowe pod kątem zmiennych. Zamiana podwójnych cudzysłowów na apostrofy daje kilka procent zysku.

Większy zysk można zaobserwować w przypadku użycia ciągów zawierających zmienne. Zmienne łączyć można na wiele sposobów.

$zmienna1 = "$a $b $c";
$zmienna2 = $a.' '.$b.' '.$c;

Z porównania wydajności, ta druga metoda wykazuje prędkość już o kilkanaście procent wyższą.

Wyświetlanie

Metodę wysyłania danych do użytkownika należy dobrać w zależności od tego jakie dane będą wysyłane. Ważna jest także czytelność kodu.

W przypadku wysyłania danych nie zawierających zmiennych, jak na przykład kod HTML, największą efektywność uzyskuje się używając instrukcji echo, dane obejmując w apostrofy. Przewaga nad wychodzeniem z trybu PHP tagiem ?>, wyświetlaniem danych i ponownym wchodzeniem w tryb PHP jest niewielka.

Przy wyświetlaniu danych zawierających zmienne, najlepiej jest zamykać tryb PHP i otwierać go jedynie aby wyświetlić wartość zmiennej.

?>
<div><?php echo $a?> to <?php echo $b?> test <?php echo $c?></div>
<?php

Ważne jest, aby przy wyświetlaniu wartości zmiennej nie obejmować jej w cudzysłowy. Cytowanie zmiennej powoduje znaczne obniżenie wydajności. Użycie skróconej notacji wyświetlania:

 <?= $zmienna ?>

jest całkowicie równoznaczne użyciu instrukcji echo, także pod względem wydajności.

Funkcja printf() jest o wiele mniej wydajna niż instrukcja echo czy wyświetlanie danych poza trybem PHP. Użycie tej funkcji jest zasadne tylko i wyłącznie jeśli wykorzystane będą zaawansowane możliwości formatowania, które dostarcza ta funkcja.

<?php
$cena    = 32.95;
$ilosc   = 4;
$wartosc = $cena * $ilosc;
printf ('Wartość to %01.2f', $wartosc);
?>

W przykładzie tym wykorzystano funkcję printf() do wyświetlenia liczby w formacie walutowym.

Niepotrzebne zmienne

Każda nowo tworzona zmienna to pewne obciążenie. Obciążany jest procesor, który ma więcej kodu do przetworzenia a później większą tablicę zmiennych do przeszukania, jak również i pamięć, w której przechowywana będzie zmienna. Poniższy przykład ilustruje zastosowanie całkowicie zbędnej zmiennej.

<?php
function kwadrat($liczba)
{
   $wynik = $liczba * $liczba;
   return $wynik;
}

$dwaKwadrat = kwadrat(2);
echo $dwaKwadrat;
?>

Wyraźnie widać, że można się obejść bez tej zmiennej definiując funkcję w taki sposób:

<?php
function kwadrat($liczba)
{
   return $liczba * $liczba;
}

echo kwadrat(2);
?>

Użycie zmiennej do zachowania wyniku działania funkcji jest zasadne tylko jeśli wynik ten będzie potrzebny w dalszej części skryptu.

Mimo wszystko czasem warto jest poświęcić trochę zasobów na zwiększoną czytelność kodu. Dobrze jest skorzystać ze zmiennych pomocniczych na przykład przy długich wyrażeniach regularnych.

Przed zastosowaniem tymczasowej zmiennej trzeba sobie zadać 2 pytania:

  • Czy użyję tej zmiennej więcej niż raz?
  • Czy czytelność kodu zostanie znacznie zwiększona?

Jeśli odpowiedź na przynajmniej jedno z tych pytań brzmi „tak”, wtedy zasadne jest użycie tej zmiennej.

Niepotrzebne zmienne, zwłaszcza te bardzo duże, można usunąć za pomocą instrukcji unset(): unset($zmienna). Zmniejszy to zużycie pamięci.

Zbędne funkcje

Kiepskim pomysłem jest tworzenie funkcji, które już istnieją w PHP. Funkcje te będą mniej wydajne od tych zaszytych w kod interpretera, choćby ze względu na konieczność parsowania tej funkcji. Każdy programista powinien zapoznać się z podręcznikiem PHP, a przynajmniej ze skróconymi opisami funkcji, których język ten zawiera sporo. Jest to poniekąd wada PHP, lecz jeśli funkcje te już istnieją, to można z nich skorzystać.

Innym błędem jest używanie funkcji – aliasów.

<?php
function len($str) {
   return strlen($str);
}
?>

Tworzenie takich funkcji jest źródłem wielu błędów. Przede wszystkim wprowadza to bałagan w kodzie. Skrypt staje się hermetyczny i nikt oprócz autora nie będzie w stanie go zrozumieć. Tworzenie dodatkowych funkcji o identycznej funkcjonalności powoduje złe przyzwyczajenia. Większa liczba funkcji to także spowolnienie kodu.

Niepotrzebne wywołania

Każde użycie „drogiej” funkcji, zużywającej wiele zasobów, powinno być przemyślane. Jeśli wynik działania funkcji używany jest w kilku miejscach bądź w pętli, dobrze jest wynik działania tej funkcji zapamiętać a następni korzystać już z tej zmiennej.

Klasycznym przypadkiem jest użycie funkcji count() przy sprawdzaniu warunku w pętli for użytej do „przejścia” po tablicy.

<?php
$arr = Array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
for ($i = 0; $i < count($arr); $i++) {
   echo $arr[$i];
}
?>

Funkcja count() wykonywana jest przy każdym przebiegu pętli. Zamiast tego wynik działania tej funkcji można zapisać do zmiennej w sekcji inicjalizującej pętlę i skorzystać z niej w warunku.

<?php
(...)
for ($i = 0, $count = count($arr); $i < $count; $i++) {
(...)
?>

W takim przypadku warto jest też rozważyć użycie pętli foreach. Jest ona minimalnie wolniejsza od pętli for, lecz przy przechodzeniu przez tablice jest ona o wiele wygodniejsza, zwłaszcza przy tablicach z kluczami tekstowymi.

Programowanie obiektowe

Jak powszechnie wiadomo, programowanie obiektowe w PHP mocno kuleje. Kod pisany obiektowo jest mocno spowoniony. PHP ma także niewiele z mechanizmów obiektowych, znanych z innych języków programowania. Dlatego też nie ma jeszcze wielu przesłanek żeby pisać obiektowo w PHP. Ma się to zmienić wraz z wypuszczeniem stabilnej wersji Zend Engine 2.0, co według najnowszych informacji ma nastąpić na początku 2003 roku. Tymczasem trzeba się mocno zastanowić, czy użycie OOP w skrypcie jest na prawdę niezbędne.

Użycie klas do grupowania funkcji powoduje bardzo duży narzut czasowy. Lepiej jest pomyśleć o przedrostku dodawanym do nazwy. Jeśli jednak użycie programowania obiektowego jest z jakichkolwiek przyczyn niezbędne, warto pamiętać o kilku zasadach.

  • należy inicjalizować wszystkie używane zmienne
  • jeśli zmienne globalne lub pola klasy są używane z metodzie więcej niż 2 razy, dobrze jest przypisać je do zmiennych lokalnych
  • częściej używane metody powinny być umieszczone w klasach potomnych

Operacje na plikach

Przy pisaniu skryptów operujących na plikach, niezbędne jest zadanie sobie pytania „Czy zachodzi potrzeba wczytania całego pliku do skryptu?”. Jeśli tak, można skorzystać z łatwego i szybkiego mechanizmu:

$dane = implode('', file('plik'));

Mechanizm ten jest szybki, lecz zabiera dużo pamięci. Jeśli skrypt ma być przystosowany do pracy pod dużym obciążeniem, lepiej jest skorzystać z funkcji fopen(), fgets() i innych:

<?php
$fp = fopen('plik', 'r');
while (!feof($fp)) {
   przetworz_linie(fgets($fp));
}
fclose($fp);
?>

To rozwiązanie nie wymaga przechowywania w pamięci całego pliku. Jest to ważne przy jednoczesnej pracy wielu instancji tego skryptu. Zużycie całej pamięci fizycznej doprowadzi do korzystania z pamięci wirtualnej, przez co prędkość działania skryptu znacznie spadnie.

Złym pomysłem jest korzystanie z funkcji file() jeśli zachodzi potrzeba odczytania tylko jednej linii z pliku. Takie rozwiązanie:

<?php
$plik = file('plik');
$szukana_linia = $plik[1];
?>

jest bardzo nieefektywne. Wczytywany jest cały plik, podczas gdy potrzebna jest tylko jedna linia. Wydajniejszy jest poniższy kod. Wydajność zależy od wielkości pliku – np. dla pliku zawierającego 1000 linii zysk jest około 400-krotny.

<?php
$fp = fopen('plik', 'r');
fgets($fp);
$szukana_linia = fgets($fp);
fclose($fp);
?>

Wyrażenia regularne

Wyrażenia regularne kuszą dużymi możliwościami, lecz możliwości te są okupione długim czasem wykonywania. Ich użycie musi być dobrze przemyślane. Często okazuje się, że do wykonania danego zadania, zamiast wyrażeń regularnych, wystarczy skorzystać z funkcji obsługi stringów. Poniższe funkcje bardzo często mogą zastąpić wyrażenia regularne.

strtr()
działa podobnie jak str_replace(), lecz operuje na pojedynczych znakach
strtoupper()
zamienia wszystkie małe litery na duże
strtolower()
zamienia wszystkie duże litery na małe
ucfirst()
zamienia wszystkie pierwsze litery wyrazów na duże
trim(), ltrim(), rtrim(), chop()
funkcje te służą do usuwania białych znaków (spacji, znaków tabulacji, znaków nowej linii) z początku, końca lub z jednej i drugiej strony, zależnie od funkcji

Częstym błędem jest użycie funkcji ereg_replace() lub preg_replace() do prostej podmiany jednego słowa na inne. Jest to błąd kosztujący sporo czasu. Użycie zamiast tego funkcji str_replace() daje zysk o co najmniej 25%. A im dłuższy jest string, w którym słowa mają być podmienione, tym większy jest zysk na wydajności.

Warto tu zaznaczyć, że funkcje kompatybilne z Perlem są generalnie szybsze od tych kompatybilnych ze standardem POSIX.

Jednak nie w każdym zastosowaniu funkcja str_replace() jest efektywniejsza niż *reg_replace(). Przykładem może być prosty system template’ów, w którym zmienne do podmiany podawane są w postaci {ZMIENNA}. Funkcja wypełniająca wzorzec może przyjmować jako parametr tablicę, w której kluczami są nazwy zmiennych a wartościami – wartości które mają być wstawione do wzorca. Takie rozwiązanie jest wygodnie jeśli nazwy pól wzorca odpowiadają nazwom pól w tabeli – wtedy wystarczy do funkcji przekazać wiersz odczytany z bazy. Użycie do podmiany serii wywołań str_replace() czy nawet jednego wywołania, w którym pierwsze dwa parametry są tablicami. Z testów wynika, że dużo efektywniejsze jest takie rozwiązanie:

preg_replace('/{([^}]+)}/e', '$dane["\1"]', $template);

Nadużywane są funkcje sprawdzające, czy wyrażenie regularne pasuje do zmiennej. Często funkcja ereg() lub preg_match() wykorzystywana jest do sprawdzenia czy zmienna zawiera pewne słowo lub znak. Do takich celów stworzone zostały funkcje strpos(), strrpos(), strrchr(), strstr() i stristr(). Są one dużo wydajniejsze od wyrażeń regularnych w tych zastosowaniach.

Zastosowanie funkcji split() lub preg_split() jest w wielu miejscach niepotrzebnie. Funkcje te są używane do dzielenia stringów według podanego separatora, podczas gdy posiadają one o wiele większe możliwości. Wielu programistów nawet nie wie, że funkcja split() potrafi dzielić stringi według wyrażenia regularnego. explode() dzieli tylko i wyłącznie na podstawie ciągów znaków, przez co funkcja ta jest znacznie szybsza. Także w tym przypadku funkcja kompatybilna z Perlem, preg_split(), jest szybsza od funkcji POSIXowej, split().

Buforowanie wyjścia

Pojedyncze, zbiorcze wysłanie danych do użytkownika jest znacznie wydajniejsze niż wiele małych. Można zamiast wysyłać dane do użytkownika zapisywać je do zmiennej, a na końcu skryptu ją wyświetlić, lecz lepiej skorzystać z mechanizmów wbudowanych w PHP.

Output bufferring, czyli buforowanie wyjścia, to fachowa nazwa na mechanizm opisany wyżej. PHP, zamiast wysyłać dane od razu do przeglądarki, zapamiętuje je, a wysłanie ich następuje dopiero w momencie jawnego żądania bądź po zakończeniu wywołania skryptu.

Aby włączyć buforowanie wyjścia wystarczy na samym początku skryptu wywołać funkcję ob_start().

Buforowanie wyjścia załatwia także dwa inne problemy. Po pierwsze, jako że pomimo wywołania funkcji echo() dane nie są wyświetlane, można wysłać nagłówek HTTP do przeglądarki w dowolnym miejscu kodu. Po drugie, dzięki buforowaniu wyjścia możliwa jest kompresja danych wysyłanych do przeglądarki. Na pewno ucieszą się z tego użytkownicy modemów (czyli m. in. ja). Wystarczy funkcję uruchamiającą buforowanie wywołać z odpowiednim parametrem: ob_start(„ob_gzhandler”). Funkcja obsługi kompresji sama rozpozna jaki typ kompresji obsługuje przeglądarka i wyśle odpowiednie dane. Jeśli przeglądarka w ogóle nie obsługuje kompresji, wysłane zostaną nieskompresowane dane.

Całe zagadnienie jest dokładnie opisane w podręczniku PHP.

Cache

Jak łatwo można się domyśleć, szybciej serwowane są strony statyczne niż takie, które muszą być wygenerowane przez PHP, zwłaszcza jeśli dane są pobierane z bazy. Oczywiste więc wydaje się takie rozwiązanie, żeby strony były generowane tylko raz, kiedy dane się zmieniają. Stronę taką można zapisać na dysku i serwować ją zamiast generować.

Klas realizujących Cache jest wiele. Można taką znaleźć nawet w zasobach PEAR (repozytorium kodu PHP – http://pear.php.net/). Nie jest trudno napisać taki kod samemu. Kluczowym elementem jest output buferring. Funkcje do obsługi buforowania wyjścia można wykorzystać do zapisania w pliku tego, co skrypt chce wyświetlić. Dzięki temu cache można stworzyć nie zmieniając reszty kodu strony. Trzeba się też zastanowić kiedy cache musi być odświeżany – przy modyfikacji pliku lub bazy danych, na podstawie daty modyfikacji każdego z plików czy też skrypt powinien odświeżać regularnie co jakiś czas wszystkie pliki.

Dobrze jest przejrzeć kod kilku pakietów dostarczających Cache, aby zorientować się o co w tym wszystkim chodzi.

Obsługa sesji

Przy skryptach intensywnie używających sesji warto jest zamienić standardowe funkcje obsługi na alternatywne. W Internecie można znaleźć funkcje zapisujące dane sesyjne w bazach danych, lecz o wiele wydajniejszy jest handler zapisujący dane w pamięci dzielonej.

Aby uaktywnić ten sposób przechowywania danych niezbędna jest kompilacja PHP z opcją „–with-mm”. Następnie w pliku konfiguracyjnym php.ini wartość dyrektywy „session.save_handler” należy ustawić na „mm”.

Zapytania SQL

Jest to temat z pogranicza PHP, lecz jest on niezwykle istotny, gdyż wykonywanie zapytać stanowi duży procent czasu poświęconego na przetwarzanie skryptu.

Optymalizacja samej struktury bazy danych to bardzo obszerny temat. Najlepiej zaopatrzyć się w dobrą książkę o teorii baz danych.

Większość serwerów baz danych, a na pewno te najczęściej używane w połączeniu z PHP, czyli MySQL i PostgreSQL, umożliwiają uzyskanie informacji na temat zapytania. Wystarczy poprzedzić je słowem kluczowym EXPLAIN.

Warto jest przejrzeć rozdział podręcznik PHP dotyczący używanej bazy danych. Można tam znaleźć informacje na temat dostępnych funkcji, które często ułatwiają pisanie skryptów, jak na przykład dostępna dla bazy MySQL funkcja mysql_insert_id() zwracająca identyfikator ostatnio wstawionego do bazy wiersza.

Wyciąganie niezbędnych pól

Wszystkie dane, które zostaną oznaczone do pobrania zapytaniem SQL muszą zostać w jakiś sposób przekazane z serwera baz danych do PHP. Problem jest mniejszy jeśli serwer WWW i baz danych jest na tym samym komputerze, lecz i tu jest to istotna kwestia.

Najłatwiej jest napisać „select * from tabela”. Jednak podanie „*” zamiast konkretnych, niezbędnych pól powoduje zwiększone obciążenie bazy danych, jak również spowolnienie działania skryptu przez konieczność przesłania większej ilości danych.

Ograniczanie wyników

Klauzula WHERE także wymaga przemyślenia. Każdy z warunków to sprawdzenie zawartości pewnego pola we wszystkich rekordach (to jest pewne uproszczenie, ale na potrzeby artykułu wystarczy). Klauzulę warunkową należy skonstruować tak, żeby pierwszy warunek odrzucił jak najwięcej rekordów. Dzięki temu każde następne sprawdzenie musi przebrnąć przez mniejszą ilość rekordów.

Używanie PHP do sortowania

Po co używać PHP do posortowania wyciągniętych z bazy danych, jeśli może to zrobić sama baza? Sortowanie danych przez PHP wiąże się z zapisaniem danych do tablicy, co powoduje zwiększone obciążenie pamięci i procesora. Wystarczy zrezygnować z funkcji sortujących na rzecz klauzuli „ORDER BY pole”, która wymusi na serwerze baz danych podanie rekordów w określonej kolejności. Działanie tej klauzuli może się nieznacznie różnić na różnych typach serwerów baz, więc najlepiej sięgnąć do dokumentacji tego serwera.

Zliczanie wierszy

Jest wiele możliwości sprawdzenia ile wierszy jest w tabeli bądź ile spełnia podane warunki. Najgorszą z nich jest wykonanie zapytania „select * from tabela where warunek” a następnie sprawdzenie za pomocą odpowiedniej funkcji PHP ile wierszy zwróciła baza. Rozwiązanie to wymusza przesłanie wszystkich rekordów spełniających warunek do PHP i przechowywanie ich w pamięci. Oczywiście jest to bardzo nieefektywne. O wiele lepiej jest skorzystać z funkcji count(*), która jest dostępna w większości serwerów baz danych. Zapytanie sprawdzające ilość wierszy spełniających warunek wygląda tak: „select count(*) from tabela where warunek”. Teraz wystarczy odczytać wartość jednego pola z pojedynczego rekordu zwróconego przez zapytanie.

Na koniec

Nic nie zastąpi zdrowego rozsądku. Dobrze jest przejrzeć kod w poszukiwaniu błędów. Warto poprosić o przegląd znajomego kodera – nie raz autor skryptu nie widzi błędu, który ktoś inny zauważa na pierwszy rzut oka.

Pisanie skryptów PHP mocno rozleniwia programistę, więc trzeba włożyć sporo wysiłku w wyrobienie w sobie dobrych nawyków.

Autor: leafnode

Architekt oprogramowania webowego, programista, analityk bezpieczeństwa serwisów internetowych, speaker, konsultant. Potrzebujesz pomocy? Skontaktuj się ze mną!

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *