Jak pisałem w poprzednim wpisie (“Hello world” w kilkudziesięciu językach (programowania)), postanowiłem sprawdzić, jakie są różnice pomiędzy echo i print w PHP. Poniżej możecie przeczytać co nieco o echo i print, o różnicach między nimi i zobaczyć wyniki testów. Mnie nie zaskoczyły wcale. Wręcz nadal szukam sensu sprawdzania tego :)
Czym jest echo i print
Podczas pisania poprzedniego wpisu zacząłem się zastanawiać jakie są różnice pomiędzy echo i print w PHP. Ja zazwyczaj używam echo. Ale czy słusznie?
Echo i print nie są funkcjami, są to tak zwane elementy składni języka, nie ma zatem potrzeby (choć można) używania nawiasów okrągłych w celu podania argumentów. Prawidłowe są obie formy:
echo "Hello World!"; echo("Hello World!"); print "Hello World!"; print("Hello World!");
Użycie nawiasów nie wynika jednak z konstrukcji tych elementów. Można ich użyć tu tak samo, jak każą inną wartość można objąć takimi nawiasami: (5), ($string), -(3 + 2), …
Różnice
Obie konstrukcje mało się od siebie różnią i robią dokładnie to, czego od nich oczekujemy – podają na wyjście zawartość wszystkich argumentów. Czy kogoś to zaskoczyło? ;)
Pierwszą z różnic jest ilość argumentów – print może przyjąć dokładnie jeden argument, echo dowolną ich ilość oddzielonych przecinkami. Wynika to z faktu, że choć print nie jest funkcją, zachowuje się jak funkcja, a funkcje mogą przyjmować tylko z góry określoną ilość parametrów – print przyjmuje zawsze 1. Funkcją jednak nie jest np. dlatego, ze nie wymaga nawiasów okrągłych w których przekazywane są argumenty (funkcje wymagają). Aby przy pomocy print zwrócić wartość kilku argumentów, należy je wcześniej połączyć w jeden ciąg:
echo "test ", $string1, "test ", $string2, " test"; //kilka arg echo "test $string1 test $string2 test"; echo 'test '.$string1.'test '.$string2.' test'; print "test $string1 test $string2 test"; print 'test '.$string1.'test '.$string1.' test';
I tu jeszcze jedna uwaga – Pierwszy przykład (wiele argumentów) jest możliwy tylko bez użycia nawiasów okrągłych. Gdy użyjemy takich nawiasów, PHP traktuje to jako pojedyncze wyrażenie, a znak przecinka nie jest operatorem. To nie działa tak, jak w przypadku funkcji.
Druga różnica to zwracane wartości. Echo nie zwraca nic, print zwraca zawsze wartość “1″:
<?php var_dump(print "Hello World!");
W wyniku dostajemy:
Hello World!int(1)
Z powodu zwracania zawsze tej samej wartości przez print, jest to raczej mało przydatna własność.
Fakt, iż print zwraca wartość powoduje, że print działa bardziej jak funkcja i może być użyte w kontekście funkcji. Echo zaś działa bardziej jak element składni języka. To jest kolejna różnica, choć powiązana z poprzednią. Przykład:
<?php if(echo "Hello World!") {...} //nie zadziała if(print "Hello World!") {...} //zadziała $a = 10 + print 7; //jaki będzie wynik? 8 :) print print print print 7; // 7111
Czwarta różnica, to… ilość liter w konstrukcji – echo ma ich 4 a print 5. Tak, wiem, naciągane, ale mówimy o wszystkich różnicach :)
Jest jeszcze różnica w wykonywanych tzw. opkodach przez PHP ale o tym niżej.
No i szósta różnica, jak się domyślacie, to czasy wykonywania.
Metodologia testów
Testy szybkości działania obu konstrukcji językowych zostały przeprowadzone w kilku etapach.
Pierwszy etap, to szkielet służący do testowania szybkości działania. Prosty test echo wygląda tak:
<?php // require benchmark file require("benchmark.php"); $time1 = microtime(TRUE); //code to be benchmarked for($i = 0; $i < $repeats; $i++){ echo "Hello World!"; } $time2 = microtime(TRUE); // empty loop for($i = 0; $i < $repeats; $i++){ } $time3 = microtime(TRUE); // save log logResult();
Test ze zmiennymi w argumentach przykładowo tak:
<?php // require benchmark file require("benchmark.php"); $time1 = microtime(TRUE); //code to be benchmarked $string1 = "test1"; $string2 = "test2"; for($i = 0; $i < $repeats; $i++){ echo "test ", $string1, "test ", $string2, " test"; } $time2 = microtime(TRUE); // empty loop for($i = 0; $i < $repeats; $i++){ } $time3 = microtime(TRUE); // save log logResult();
Test polega na wykonaniu w pętli określoną ilość razy testowanego kodu – w tym przypadku jest to 1 milion (1 000 000). Zapisywany jest czas tuż przed rozpoczęciem pętli i tuż po jej zakończeniu. Następnie wykonywana jest pusta pętla taką samą ilość razy (i również zapisany zostaje czas), aby na tej podstawie móc wyliczyć czas potrzebny na wykonanie samego testowanego kodu (poprzez odjęcie czasu drugiej pętli od pierwszej).
Takie rozwiązanie obarczone jest jednak pewnym problemem (ale praktycznie każde rozwiązanie ma jakiś “minus”). Zbyt długie wykonywanie się pierwszej pętli może wydłużyć przestawiony czas wykonania testowanego kodu, a zbyt długie wykonywanie drugiej może ten czas skrócić poniżej czasu wykonywania się kodu testowego, lub w skrajnych przypadkach różnica teoretycznie może wynieść poniżej zera, co oczywiście normalnie nie jest możliwe. Chodzi o przypadki, w których podczas testu coś chwilowo obciąży procesor. Jest oczywiście pewne rozwiązanie tego problemu, ale o tym niżej.
Jeszcze jedna informacja – w przypadku kodu z argumentami, zmienne ustawiane są przed pętlą – nie ma potrzeby ustawiania ich za każdym razem, bo nie to jest przedmiotem testów.
Testów jest w sumie 11 (dokładny opis znajduje się niżej przy analizie wyników):
echo "Hello World!"; print "Hello World!"; echo("Hello World!"); print("Hello World!"); echo 'Hello World!'; print 'Hello World!'; print "test $string1 test $string2 test"; echo "test $string1 test $string2 test"; echo "test ", $string1, "test ", $string2, " test"; echo 'test '.$string1.'test '.$string2.' test'; print 'test '.$string1.'test '.$string2.' test';
Testy wykonywane są kolejno. Dodatkowo każda seria 11 testów powtarzana jest 20 razy przy pomocy prostego skryptu w bash-u (20 testów daje możliwość odrzucenia statystycznie nieprawidłowych wyników):
#!/bin/bash for (( j=1; j<=20; j++ )) do for i in {1..11} do php test$i.php > /dev/null done done
Wyjście ze skryptu PHP zostało przekierowane do /dev/null, aby wyświetlanie efektu działania echo i print nie przekłamywało wyniku.
Podsumowując – każdy testowany kod uruchamiany jest określoną liczbę razy. Takich testów jest 11 i wykonywane są kolejno, a całość dodatkowo wykonana jest 20 razy. Wynikiem jest 11 plików z czasami (dla każdego testu po jednym), a każdy plik zawiera 20 pomiarów.
Jak zauważyliście, każdy z plików php (dla każdego testu) dołącza benchmark.php:
<?php // globals global $time1; global $time2; global $time3; global $repeats; // number of repeats $repeats = 1000000; // calculate time and save result function logResult(){ // globals global $time1; global $time2; global $time3; global $repeats; // calculate time $time = 2*$time2 - $time1 - $time3; // append result to file // (eg. test1.php.1000000.results.txt) file_put_contents("results/{$_SERVER['SCRIPT_NAME']} .$repeats.results.txt", "$time\n", FILE_APPEND); } // save calculated average time function logAverage($result){ file_put_contents("results.txt", $result, FILE_APPEND); } // calculate average time function average($file){ // loda times from file $times = file($file); // calculate average $average = array_sum($times)/count($times); // calculate standard deviation $deviation = 0; foreach($times as $time){ $deviation += pow($time-$average, 2); } $deviation = sqrt($deviation/count($times)); // reject times obove standard deviation $newTimes = array(); foreach($times as $time) if(abs($time - $average) < $deviation) $newTimes[] = $time; // calculate and return average return array_sum($newTimes)/count($newTimes); }
Jest to plik zawierający ustawienie zmiennych z czasami do testów (jako globalne), zmienna z ilością powtórzeń każdego testu (ustawiłem 1 milion), funkcja logResult() która wylicza czas wykonywania testowanego kodu (od czasu wykonania pierwszej pętli z testowanym kodem odejmuje czas wykonania drugiej, pustej pętli) i zapisuje w pliku z wynikami dla danego testu (tutaj widać, dlaczego dla każdego testu jest tylko jeden plik – wynik jest dopisywany na końcu pliku).
Druga część tego pliku służy późniejszej analizie danych – funkcja logAverage() która zapisuje końcowe wyniki, oraz funkcja average() wyliczająca średnią.
Ostatni krok to przeanalizowanie każdego z 20 wyników zawierającego czas 1 miliona powtórzeń. Robi to ten kod:
<?php // require benchmark file require("benchmark.php"); // load list of log files $logs = scandir('results'); foreach($logs as $log){ // check if not a log file if(pathinfo("results/$log", PATHINFO_EXTENSION) != "txt") continue; // calculate and save average $average = average("results/$log"); logAverage("$log\n$average\n\n"); }
Ładowany jest każdy plik z wynikami (dla każdego testu), a następnie wyliczana jest średnia, ale nie taka zwykła średnia arytmetyczna.
Jak wspomniałem wyżej, za wyliczenie średniej odpowiada funkcja average() z benchmark.php. Funkcja ta wylicza średnią arytmetyczną ze wszystkich wyników, następnie wylicza odchylenie standardowe, na podstawie którego odrzuca wszystkie wyniki znajdujące się poza odchyleniem standardowym od średniej arytmetycznej (niepewność standardowa pomiaru), po czym ponownie wylicza średnią z pozostałych wyników. Ma to na celu wyeliminowanie wyników znacznie odbiegających od średniej, co zakłócało by wynik pomiaru. Dlatego też postanowiłem każdy test powtórzyć 20 razy, aby móc porównać ze sobą wyniki i ocenić, czy nie odbiegają od odchylenia standardowego.
Na końcu całego procesu otrzymywany jest plik z wynikami – zawiera parę składająca się z nazwy testu i średnią czasu wykonywania.
Wyniki i analiza
Wyniki zostały podzielone na dwie grupy – pierwsza z nich to pierwsze 6 testów będących prostym wykonaniem echo i print, druga grupa to testy z kilkoma argumentami, aby określić który sposób jest najbardziej wydajny.
Test 1
echo "Hello World!"; 0,157704178s print "Hello World!"; 0,161651978s echo("Hello World!"); 0,157889496s print("Hello World!"); 0,162272563s echo 'Hello World!'; 0,157271266s print 'Hello World!'; 0,162129954s
Wyniki to czas wykonania miliona powtórzeń. I bardziej obrazowo:
Z testu wynika, że:
- Echo jest szybsze od print, print od echo jest wolniejszy od 2,78% do 3,18%
- Najszybsze jest echo z ciągiem w apostrofach
- Każde użycie echo jest szybsze od print
- Różnice pomiędzy różnymi sposobami użycia echo i print są na tyle małe, ze mogą być błędem pomiarowym
Test 2
print "test $string1 test $string2 test"; 0,345269183s echo "test $string1 test $string2 test"; 0,347504964s echo "test ", $string1, "test ", $string2, " test";0,849529982s echo 'test '.$string1.'test '.$string2.' test'; 0,330271993s print 'test '.$string1.'test '.$string2.' test'; 0,334040454s
Z tego testu wynika, że:
- Najszybsze jest ponownie echo (od 1,14% do 4,54% w stosunku do print)
- Najszybsze jest łączenie ciągów poprzez połączenie wszystkich ciągów operatorem kropki gdy ciągi umieszczone są w apostrofach
- Wykorzystanie możliwości podania kilku argumentów do echo jest zdecydowanie najwolniejsze ze wszystkich testów na echo
Podsumowanie testów
Jednoznacznie wygrywa zatem echo (z przewagą dla łączenia ciągów w apostrofach operatorem kropki). Trzeba tu też zwrócić uwagę na fakt, że w drugim teście łączenie ciągów następuje zanim wynik zostanie przekazany do echo czy print, poza przypadkiem, gdy echo otrzymuje wszystkie ciągi jako argumenty. Nie jest to zatem wynik działania samego echo/print, ale mimo wszystko jest szybsze, niż podawanie wszystkich ciągów jako parametry do echo.
Jeszcze jedna informacja o różnicy
VLD (Vulcan Logic Dumper) jest to ciekawe rozszerzenie do PHP, które “wpina” się do Zend Engine i zrzuca wszystkie tzw. opkody (opcodes). Opkody (w skrócie) to jednostki wykonawcze odpowiadające za wykonywanie kodu. Każde polecenie składa się na kilka opkodów.
Instalacja VLD jest prosta i sprowadza się do pobrania, kompilacji i instalacji rozszerzenia:
wget http://pecl.php.net/get/vld tar xvf vld cd vld-0.11.1/ phpize ./configure make install echo "extension=vld.so" > /etc/php5/conf.d/vld.ini /etc/init.d/apache2 restart
Zerknijmy zatem, co się dzieje podczas wykonywania echo i print:
extreme-dev-debian:~# php -d vld.active=1 -r "echo \"Hello World!\";" Finding entry points Branch analysis from position: 0 Return found filename: Command line code function name: (null) number of ops: 3 compiled vars: none line # * op fetch ext return operands --------------------------------------------------------------------------------- 1 0 > ECHO 'Hello+World!' 1 > RETURN null 2* > ZEND_HANDLE_EXCEPTION branch: # 0; line: 1- 1; sop: 0; eop: 2 path #1: 0, Hello World!
extreme-dev-debian:~# php -d vld.active=1 -r "print \"Hello World!\";" Finding entry points Branch analysis from position: 0 Return found filename: Command line code function name: (null) number of ops: 4 compiled vars: none line # * op fetch ext return operands --------------------------------------------------------------------------------- 1 0 > PRINT ~0 'Hello+World!' 1 FREE ~0 2 > RETURN null 3* > ZEND_HANDLE_EXCEPTION branch: # 0; line: 1- 1; sop: 0; eop: 3 path #1: 0, Hello World!
Widać, że dla print dochodzi jeszcze jeden opkod – “FREE”. Ma on związek z faktem, że print zwraca wartość (choć zawsze jest to wartość “1″) i następuje zwolnienie pamięci. Między innymi to jest przyczyną dłuższego wykonywania się print od echo.
Inny sposób użycia echo
Istnieje jeszcze jeden sposób użycia echo. Załóżmy, ze gdzieś w kodzie mamy ustawioną jakąś zmienną:
<?php $txt = "test"; ?>
Oczywistym jest, że w innym miejscu kodu jej zawartość można wyświetlić tak:
<?php echo $txt; ?>
Czy tak:
<?php $txt = "test"; ?>
Ale ten zapis można jeszcze bardziej skrócić:
<?= $txt; ?>
I jeszcze bardziej (bez średnika):
<?= $txt ?>
I nawet jeszcze bardziej (bez spacji):
<?=$txt?>
No dobra, da się jeszcze krócej (bez znaku równości):
<?$txt?>
Tutaj jednak mała uwaga. W wersjach PHP wcześniejszych od 5.4, aby to działało, konieczne jest włączenie short_open_tag. Od wersji 5.4 skrót z przedostatniego przykładu zawsze jest dostępny w tej formie. Ostatni przykład nie jest jednak skrótem dla <?php echo w żadnej wersji PHP, to zwykły krótki tag otwierający.
Podsumowanie
Choć echo okazało się szybsze, w niektórych wypadkach maksymalnie o 4,54% od print, to cały test i tak ma mały sens. Jeśli popatrzymy dokładniej, w najgorszym wypadku print wykonuje się 0,35 mikrosekundy a echo nieco ponad 0,33 mikrosekundy (pomijając wynik 0,85 mikrosekundy dla echo z kilkoma argumentami). W całym kodzie nie ma aż tylu użyć echo czy print, aby miało to jakiekolwiek znaczenie. Różnice pomiędzy echo i print występują, ale użyteczna jest tylko jedna – podanie wielu argumentów do echo, co nie jest możliwe dla print, ale i tak jest to wolniejsze niż standardowe połączenie stringów przed przekazaniem do echo czy print. Długość konstrukcji (4 vs 5 znaków) raczej nie ma znaczenia ;)
Jak się zatem okazuje, mogę spokojnie dalej używać echo we własnym kodzie :)
Ale po co taki test w ogóle?
jak się generuje treści statyczne to powinny być gdzieś trzymane, a nie je generować za każdym razem,
poza tym od tego są też cache jak Varnish np.
Chodziło głównie o sprawdzenie, czy lepiej używać print czy echo. Każdy używa którejś z nich i nie tylko ja się zastanawiałem co jest wydajniejsze. Potwierdzają to trafienia z Google do tego wpisu (choć mój blog powstał niedawno i w google jeszcze nie jest zbyt wysoko).
Tutaj też sprawdziłem z czystej ciekawości. Mało kto testuje takie mikrooptymalizacje, ale więcej osób się nad nimi zastanawia.
Choć ten test ma sens, jest raczej nie przydatny – nie przeczę. Ale ciekawość została zaspokojona (nie tylko moja).
Ja nie miałem tu na myśli generowanie contentu za każdym razem. Cache to dla mnie nie nowość oczywiście :)
Hm, widziałem podobne testy, tylko nie pamiętam czy na PL czy EN stronach, ale skoro już testujesz to jeszcze możesz sprawdzić składnie heredoc vs taka z wstawianiem zmiennych w podwójne uszy, tj
$d = <<<EOT
{$dupa}
EOT;
$d = "$dupa"
Ja też widziałem podobne testy, ale wg. mnie nie były dokładne (np. zawierały czas wykonywania się pętli).
Z resztą czasami warto zrobić coś samemu i nie ważne, że ktoś inny też już to zrobił.
Nad heredoc się zastanowię, bo nie planuję w najbliższym czasie robić aktualizacji tego wpisu :)
Fajny art., ale szkoda że nie napisałeś dla czego echo(“xxx”) jest wolniejsze od echo “xxx”;
Bo o ile większość wyników była do przewidzenia, to ten akurat jest dla mnie zaskakujący – nie ukrywam że PHP to nie jest język, z którym mam na co dzień do czynienia, i być może różnica między tymi dwoma konstrukcjami jest oczywista, ale nigdzie nie potrafiłem się tego doszukać. Mógłbyś to wyjaśnić?
Dzięki :)
Rozumiem, że pytasz o 2 wniosek 1 testu? Zerknij na 4 wniosek. Różnica czasów jest tak niewielka, że spokojnie może być błędem pomiarowym.
Jeśli pytasz o coś innego, napisz dokładniej :)