echo vs. print – co szybsze i jakie są różnice?

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:

  1. Echo jest szybsze od print, print od echo jest wolniejszy od 2,78% do 3,18%
  2. Najszybsze jest echo z ciągiem w apostrofach
  3. Każde użycie echo jest szybsze od print
  4. 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:

  1. Najszybsze jest ponownie echo (od 1,14% do 4,54% w stosunku do print)
  2. Najszybsze jest łączenie ciągów poprzez połączenie wszystkich ciągów operatorem kropki gdy ciągi umieszczone są w apostrofach
  3. 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 :)

 

Ten wpis został opublikowany w kategorii Benchmark, PHP, Programowanie i oznaczony tagami , , , . Dodaj zakładkę do bezpośredniego odnośnika.

6 odpowiedzi na „echo vs. print – co szybsze i jakie są różnice?

  1. 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 :)

  2. Marcin Kolny pisze:

    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ć?

Dodaj komentarz

Musisz się zalogować (także Facebook, Google+, Twitter), aby móc dodać komentarz.