Translate: 
EnglishFrenchGermanItalianPolishPortugueseRussianSpanish

Zastępcy eval w PHP: czyli jak szybciej zrobić sobie coś złego

Zapewne wszyscy programiści PHP wiedzą, do czego służy polecenie eval() oraz to, że nie należy się nim posługiwać.

Powodów tego stanu rzeczy jest kilka, przede wszystkim jednak problemem jest bezpieczeństwo kodu i szybkość jego wykonywania. Dodatkową niedogodnością może być też fakt, że polecenie eval() nie jest obsługiwane przez kompilatory języka PHP (np. HipHop for PHP).

Istnieją niestety takie sytuacje, gdy konieczne jest wykonanie dynamiczne utworzonego kodu. W tym artykule skupię się na zagadnieniu wydajności i metodach zastąpienia polecenia eval() szybszymi odpowiednikami.

Problem wydajności

Eval jest wolny, bardzo wolny, dla zobrazowania tego problemu posłużę się przykładem:

<?php
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++) {
	++$y;
}
 
printf("<br>#0 RESULT %d, DONE IN %f", $y, microtime(true) - $start);
 
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++) {
	eval('++$y;');
}
 
printf("<br>#1 RESULT %d, DONE IN %f", $y, microtime(true) - $start);

Po uruchomieniu tego skryptu otrzymałem takie wyniki:

#0 RESULT 100000, DONE IN 0.039707
#1 RESULT 100000, DONE IN 0.943396

Jak widać, różnica czasu jest ogromna. Użycie polecenia eval spowalnia wykonywanie pętli ponad dwudziestokrotnie!.

Na szczęście istnieją sposoby, aby przyspieszyć wykonywanie dynamicznego kodu.

Funkcja create_function

Funkcja ta jest dobrze znana osobom wykonującym skomplikowane operacje na tablicach danych. To właśnie w nich używana jest często jako swego rodzaju namiastka funkcji anonimowej znanej z JavaScript. Niestety niewielu programistów używa tej funkcji jako zamiennika dla polecenia eval(), a szkoda, gdyż jest ona o wiele bardziej wydajna.

Na dowód posłuży nam następujący skrypt PHP:

<?php
$function = create_function('$y', 'return ++$y;');
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++) {
	$y = $function($y);
}
 
printf("<br>#2 RESULT %d, DONE IN %f", $y, microtime(true) - $start);

Wynik uzyskany powyższym skryptem jest następujący:

#2 RESULT 100000, DONE IN 0.100959

Użycie create_function() przyniosło wymierne korzyści. Skrypt wykonał się dziewięć razy szybciej niż w przypadku polecenia eval(). Jest to niestety nadal prawie trzykrotnie wolniej, niż podczas wykonywania operacji bezpośrednio w kodzie.

Otrzymany wynik można jednak odrobinę poprawić prostą sztuczką programistyczną.

Wymiana danych poprzez referencje

Wydajność powyższego skryptu można poprawić, pozbywając się z niego nadmiarowego operatora przypisania wartości.

W tym przypadku kod źródłowy wygląda następująco:

<?php
$functionRef = create_function('& $y', '++$y;');
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++) {
	$functionRef($y);
}
 
printf("<br>#3 RESULT %d, DONE IN %f", $y, microtime(true) - $start);

Po jego uruchomieniu pojawia się następujący wynik:

#3 RESULT 100000, DONE IN 0.085249

Po zastosowaniu referencji czas wykonywania skryptu zmalał z 0.100959 do 0.085249 sekundy. Jest to niewiele ponad dwa razy wolniej niż przy dodawaniu wykonanym bezpośrednio w pętli for. Niestety jest to też dolna granica optymalizacji, gdyż pozostałe spowolnienie wynika z użycia funkcji PHP.

Ślepe uliczki optymalizacji

Do takich należą między innymi próby uruchomienia dynamicznego kodu poleceniami include(), require():

<?php
$base64 = 'data://text/plain;base64,' . base64_encode('<?php '.$eval);
stream_wrapper_register('test', 'PluggableStream');
file_put_contents('test://test',  '<?php '.$eval);
 
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++) {
	include('test://test');
}
 
printf("<br>#4 RESULT %d, DONE IN %f", $y, microtime(true) - $start);
 
$start = microtime(true);
$y = 0;
for($i = 0; $i < $count; $i++) {
	include($base64);
}
 
printf("<br>#5 RESULT %d, DONE IN %f", $y, microtime(true) - $start);

Otrzymane czasy są w tym przypadku tragiczne:

#4 RESULT 100000, DONE IN 14.634518
#5 RESULT 100000, DONE IN 10.187585

Warto przy tym zauważyć, że własne stream wrappery są mniej wydajne, niż użycie kodowania base64 dostępnego od PHP 5.1.

Ograniczenia dynamicznego kodu PHP

Do tej pory zidentyfikowałem następujące problemy z dynamicznym kodem:

  1. niższa wydajność niż statycznego kodu
  2. problemy z bezpieczeństwem kodu (ryzyko code injection)
  3. problemy z debuggowaniem kodu (mylne komunikaty błędów)
  4. brak dostępu do lokalnych zmiennych w create_function() (wymaga użycia extract()/compact())
  5. ograniczone wsparcie dla namespace w przypadku create_function() i PHP 5.3+
  6. brak wsparcia ze strony kompilatorów PHP (HipHop for PHP)
  7. błędna obsługa dynamicznego kodu uruchomionego poprzez include/require w akceleratorze Zend Optimizer
  8. brak cache opcodu w przypadku akceleratorów PHP (np. APC, eAccelerator)

Podsumowanie

Poniżej przedstawiam zbiorcze zestawienie testów:

Nazwa testu Czas wykonywania (sek). Czas wykonywania (%)
kod statyczny (test #0) 0.039707 100,00%
eval (test #1) 0.943396 2375,89%
create_function (test #2) 0.100959 254,26%
create_function (ref) (test #3) 0.085249 214,70%

Lista wyników oraz potencjalnych problemów jasno uwidacznia, dlaczego dynamiczny kod PHP uważany jest za zło ostateczne. W skrajnych przypadkach funkcjonalność ta jest jednak nie do uniknięcia: mam więc nadzieję że artykuł ten wyjaśnił jak wybrać mniejsze zło i wyrządzić sobie wtedy jak najmniejszą krzywdę.

Dodaj odpowiedź