Translate: 
EnglishFrenchGermanItalianPolishPortugueseRussianSpanish

Co najbardziej spowalnia skrypty PHP?

Pierwszą, oczywistą przyczyną jest to, że język PHP, jest językiem interpretowanym, a nie kompilowanym. Nie tłumaczy to jednak kiepskich wyników porównawczych z innymi językami tego samego typu, takimi jak Perl czy Python, które w wielu scenariuszach wykazują o wiele większą wydajnością.

Co sprawia, że język PHP jest od nich wolniejszy?

Przede wszystkim funkcje!

Przeglądając kody różnego typu „benchmarków” porównawczych okazuje się, że największe różnice widoczne są w przypadku wykonywania intensywnych czynności bazujących na pętlach, funkcjach, oraz rekurencji.

Wspólnym mianownikiem dla wymienionych operacji są oczywiście funkcje. O ile wydajność pętli można łatwo poprawić poprzez opisane w innym artykule optymalizacje, o tyle uniknięcie problemów związanych z funkcjami nie jest już takie proste.

Oto prosty przykład obrazujący problem z funkcjami:

<?php
 
/**
 * Funkcja sumujaca ze soba dwie liczby
 * 
 * @param int $x
 * @param int $y
 * @return int
 */
function add($x, $y) {
    return $x + $y;
}
 
// liczba powtorzen w obu petlach
$max = 100000;
 
// petla nr 1:
// sumujaca wartosci bezposrednio w petli
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    $z = $count + $count;
} while($count--);
 
printf("LOOP 1 TIME %f<br>", microtime(true)-$x);
 
// petla nr 2:
// sumujaca wartosci przy pomocy funkcji
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    $z = add($count, $count);
} while($count--);
 
printf("LOOP 2 TIME %f<br>", microtime(true)-$x);

Obie pętle w powyższym kodzie mają za zadanie zrobić dokładnie to samo, czyli zsumować ze sobą wielokrotnie dwie, w tym przypadku zawsze równe sobie liczby. Oto czasy jakie otrzymaliśmy:

LOOP 1 TIME 0.008483
LOOP 2 TIME 0.057728

Jak widać czas wykonywania pętli korzystającej z funkcji add() jest siedmiokrotnie gorszy, niż tej, która wykonuje obliczenia bezpośrednio!

Czemu funkcje są powolne?

Najbardziej oczywiste jest to, że obsługa funkcji wymaga od języka PHP wykonania szeregu dodatkowych operacji: pobrania adresu funkcji, zmiany zasięgu zmiennych, przekazania parametrów wejściowych, uruchomienia kodu, ponownej zmiany zasięgu zmiennych, a na końcu zwrócenia wyniku i powrotu do głównej części programu.

To wiele dodatkowych operacji, zważywszy na to, że muszą być ponawiane kilka tysięcy razy w trakcie trwania pętli.

Szczególnie bolesne jest to w benchmarkach porównawczych polegających na wykonywaniu operacji matematycznych. Z racji tego, że PHP ma problemy z obsługą liczb zmiennoprzecinkowych, większość takich obliczeń odbywa się przy pomocy funkcji bcmath, zamiast zwykłych operatorów +-/*.

Czy można temu zaradzić?

Znając już powody wolnego działania funkcji, spróbujmy rozwiązać problem ich wydajności. W końcu zła wydajność funkcji w PHP nie ma tak dużego odzwierciedlenia w przypadku innych języków: Perl i Python używają funkcji do obsługi programu, a działają w takim przypadku szybciej.

O ile nie mamy wpływu na narzut spowodowany zmianą zasięgu zmiennych, to możemy sprawdzić wpływ ilości argumentów wejściowych na ogólną wydajność programu.

W tym celu zmodyfikujemy trochę poprzedni przykład:

/**
 * Funkcja sumujaca ze soba dwie liczby
 * 
 * @param int $x
 * @param int $y
 * @return int
 */
function add($x, $y) {
    return $x + $y;
}
 
/**
 * Funkcja sumujaca ze soba dwie liczby
 * 
 * @param int $a
 * @return int
 */
function add10($a, $b, $c, $d, $e, $f, $g, $h, $i, $j) {
    return $a + $b;
}
 
// liczba powtorzen w obu petlach
$max = 100000;
 
// petla nr 1:
// sumujaca wartosci funkcja dwuargumentowa
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    $z = add($count, $count);
} while($count--);
 
printf("LOOP 1 TIME %f<br>", microtime(true)-$x);
 
// petla nr 2:
// sumujaca wartosci funkcja dziesiecioargumentowa
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    $z = add10($count, $count, $count, $count, $count, $count, $count, $count, $count, $count);
} while($count--);
 
printf("LOOP 2 TIME %f<br>", microtime(true)-$x);

Wynik, który otrzymujemy, nie jest dla nas zaskoczeniem:

LOOP 1 TIME 0.059124
LOOP 2 TIME 0.141730

Czas obsługi dziesięciu argumentów jest ponad dwa razy gorszy od wyniku uzyskanego za pomocą funkcji dwuargumentowej (przy czym nie jest ważne, czy argumenty te zostaną w niej wykorzystane, czy tylko do niej przekazane).

Wywołanie funkcji z większą ilością argumentów wejściowych przekłada się negatywnie na jej wydajność.

W tym miejscu warto jeszcze wspomnieć o często spotykanych praktykach przekazywania wartości przez referencję. W Internecie krążą obiegowe opinie, że przesyłanie zmiennych w ten sposób jest szybsze niż przez wartość, gdyż unika się wtedy konieczności kopiowania danych.

Opinia ta jednak jest błędna, używanie operatora referencji (&) znacznie spowalnia wykonywanie programu, gdyż język PHP niezależnie od tego, czy zmienna przypisywana jest przez wartość, czy przez referencję wysyła do funkcji jedynie strukturę zval, która w obu przypadkach i tak odwołuje się do tej samej komórki pamięci. Kopiowanie wartości następuje jedynie podczas zmiany zawartości jednej ze zmiennych współdzielących tą strukturę danych.

Kolejnym, wskazanym przeze mnie elementem, który może negatywnie wpływać na wydajność funkcji jest zwracanie wyników do programu głównego. Zbadajmy więc to następującym skryptem:

<?php
 
/**
 * Funkcja sumujaca ze soba dwie liczby
 * 
 * @param int $x
 * @param int $y
 * @return int
 */
function add($x, $y) {
    return $x + $y;
}
 
/**
 * Procedura sumujaca ze soba dwie liczby
 * 
 * @param int $x
 * @param int $y
 * @return void
 */
function add2($x, $y) {
    $x + $y;
}
 
// liczba powtorzen w obu petlach
$max = 100000;
 
// petla nr 1:
// sumujaca wartosci funkcja zwracajaca wartosc
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    $z = add($count, $count);
} while($count--);
 
printf("LOOP 1 TIME %f<br>", microtime(true)-$x);
 
// petla nr 2:
// sumujaca wartosci przy pomocy procedury 
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    add2($count, $count);
} while($count--);
 
printf("LOOP 2 TIME %f<br>", microtime(true)-$x);

I ponownie nie ma zaskoczenia w wynikach:

LOOP 1 TIME 0.059272
LOOP 2 TIME 0.050447

W przypadku procedury (czyli podprogramu niezwracającego wyniku) czas był lepszy o około 15% od czasu pętli nr 1, w której zwracaliśmy wynik.

Tu jednak pewna uwaga! Wspomniana różnica w czasie nie jest spowodowana bezpośrednio samą funkcją, lecz faktem, iż w pętli numer 2 nie używamy operatora przypisania do zmiennej $z. Po uwzględnieniu tego operatora w kodzie czasy obu pętli zrównują się ze sobą.

Wydajność funkcji jest taka sama, niezależnie od tego, czy programista zwraca w niej jakąś wartość. W obu przypadkach język PHP i tak przesyła wynik do programu głównego (domyślnie NULL).

Jak więc widać, z poziomu języka PHP nie ma możliwości przyspieszenia mechanizmu obsługi funkcji. Przy pomocy pewnych zabiegów (ilość argumentów wejściowych, sposobu ich przekazania oraz treść funkcji) możemy jedynie w niewielkim stopniu niwelować problemy z wydajnością.

Czy język C jest szybszy?

Gdy wydajność PHP jest niewystarczająca, eksperci polecają przepisywanie wskazanych fragmentów kodu na język C i używanie go w postaci modułów. Sprawdźmy zatem jak sprawuje się takie rozwiązanie.

Na początek stwórzmy plik config.m4 o następującej treści:

PHP_ARG_ENABLE(agr_utils, whether to enable c_add functionality,
[ --enable-agr-utils   Enable c_add functionality])
 
if test "$PHP_AGR_UTILS" = "yes"; then
  AC_DEFINE(HAVE_AGR_UTILS, 1, [Whether you have c_add functionality])
  PHP_NEW_EXTENSION(agr_utils, agr_utils.c, $ext_shared)
fi

Następnie plik nagłówkowy w języku C, o nazwie agr_utils.h:

#ifndef PHP_AGR_H
#define PHP_AGR_H 1
 
#define PHP_AGR_UTILS_VERSION "1.0"
#define PHP_AGR_UTILS_EXTNAME "agragr"
 
PHP_FUNCTION(c_add);
 
extern zend_module_entry agr_utils_module_entry;
 
#define phpext_agr_utils_ptr &agr_utils_module_entry
 
#endif

I na końcu część główną modułu, plik o nazwie agr_utils.c:

#include "php.h"
#include "agr_utils.h"
 
ZEND_GET_MODULE(agr_utils)
 
static function_entry agr_utils_functions[] = {
    PHP_FE(c_add, NULL)
    {NULL, NULL, NULL}
};
 
zend_module_entry agr_utils_module_entry = {
#if ZEND_MODULE_API_NO >= 20010901
    STANDARD_MODULE_HEADER,
#endif
    PHP_AGR_UTILS_EXTNAME,
    agr_utils_functions,
    NULL,
    NULL,
    NULL,
    NULL,
    NULL,
#if ZEND_MODULE_API_NO >= 20010901
    PHP_AGR_UTILS_VERSION,
#endif
    STANDARD_MODULE_PROPERTIES
};
 
PHP_FUNCTION(c_add)
{
    long a, b;
 
    if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "ll", &a, &b) == FAILURE) {
        RETURN_NULL();
    }
    RETURN_LONG(a + b);
}

Jak widać funkcja c_add() robi dokładnie to samo, co jej PHPowy odpowiednik add() z pierwszego przykładu w tym artykule.

Teraz wystarczy tylko skompilować moduł zbiorem poleceń:

[root@devel agr]# phpize
[root@devel agr]# ./configure
[root@devel agr]# make

I po włączeniu modułu w pliku php.ini przetestować wydajność tego rozwiązania:

<?php
 
/**
 * Funkcja sumujaca ze soba dwie liczby
 *
 * @param int $x
 * @param int $y
 * @return int
 */
function add($x, $y) {
    return $x + $y;
}
 
// liczba powtorzen w obu petlach
$max = 100000;
 
// petla nr 1:
// sumujaca wartosci funkcja zwracajaca wartosc
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    $z = add($count, $count);
} while($count--);
 
printf("LOOP 1 TIME %f<br>", microtime(true)-$x);
 
// petla nr 2:
// sumujaca wartosci przy pomocy procedury
$count = $max;
$z = 0;
 
$x = microtime(true);
 
do {
    $z = c_add($count, $count);
} while($count--);
 
printf("LOOP 2 TIME %f<br>", microtime(true)-$x);

W moim przypadku otrzymane czasy wynosiły:

LOOP 1 TIME 0.046117
LOOP 2 TIME 0.036495

Funkcja napisana w języku C okazała się szybsza o 28% od jej odpowiednika w czystym języku PHP.

O ile ostatecznie udało nam się przyspieszyć wykonanie funkcji przepisując ją do języka C, to jednak nie można tego zaliczyć do spektakularnych optymalizacji. Wydajność jest niestety nadal o wiele gorsza, niż w przypadku wykonywania operacji bezpośrednio w pętli programu. Prawdziwą moc języka C można by wykorzystać dopiero po przepisaniu całej pętli do modułu lub użyciu go w bardziej zaawansowanych algorytmach.

Także i w tym przypadku warto zauważyć, że ilość argumentów wejściowych przesyłanych do funkcji c_add() ma duży wpływ na wydajność programu.

Wnioski

Silnik języka PHP posiada niestety swoje słabe strony, które ciężko przełamać nie ingerując bezpośrednio w jego kod źródłowy. Posiadając jednak świadomość tych ograniczeń, można w pewnym stopniu omijać występowanie tych problemów, poprzez odpowiednie programowanie.

Podczas programowania należy przede wszystkim:

  • unikać wywoływania funkcji w długich pętlach
  • unikać przekazywania zmiennych do funkcji poprzez referencje
  • unikać tworzenia funkcji z wieloma argumentami
  • starać się zamieniać użycie funkcji na language constructs
  • w ostateczności przepisywać funkcje do języka C

Tagi: , , , ,

Dodaj odpowiedź