piątek, 14 października 2011

Array vs Object

Tak jakoś nastał obiektowy szał i wszystko wszędzie musi być obiektowe. Oczywiście PHP ta mania nie ominęła i tak oto mamy coraz to większą społeczność krzyczących, że Object i Class jest super fajne, a Array to zło i anachroniczny przeżytek. Ale czy jest tak na pewno?
Oczywiście tablice nie pozwalają na implementacje funkcji odwołujących się jedynie do danych z konkretnej tablicy - takie coś musimy zrealizować poprzez metodę jakiejś klasy lub po prostu funkcję nie należącą do żadnej klasy (co oczywiście jest mało eleganckie). Jednak jeśli zależy nam jedynie na przechowaniu tudzież przekazaniu danych z jednej funkcji do drugiej to czy warto definiować nowy typ danych (zakładając, że struktura danych nigdy nie ulegnie zmianie)? W innych językach programowania w 99% przypadków odpowiedź brzmiała by "tak", jednak w przypadku PHP nie jest ona wcale taka oczywista.

Dlaczego? Ano załóżmy, że chcemy przesłać dane dotyczące elementu opisywanego za pomocą trzech atrybutów (id, name, color). Przykładowy kod realizujący takie zadanie wyglądałby następująco:
function getArray()
{
 $item = Array();
 $item['id'] = 1;
 $item['name'] = "Pure item";
 $item['color'] = "none";
 return $item;
}
 
function getObject()
{
 $item = new stdClass();
 $item->id = 1;
 $item->name = "Pure item";
 $item->color = "none";
 return $item;
}
function getItem()
{
 $item = new Item();
 $item->id = 1;
 $item->name = "Pure item";
 $item->color = "none";
 return $item;
}
Dla ostatniej funkcji konieczne jest zdefiniowanie typu "Item":

class Item
{
    public $id;
    public $name;
    public $color;
}

Wykonanie tych funkcji jak widać tworzy struktury o tych samych atrybutach - raz jest to tablica, a w dwóch przypadkach są to instancje klas.
getArray      0.1664547920
getObject      0.2381231785
getItem       0.2375209332
Jak widać stworzenie i zapełnienie tablicy trwa ok. 70% czasu potrzebnego na utworzenie odpowiadającej jej klasie. Warto zauważyć, że czasy potrzebne na utworzenie instancji obiektu za pomocą zdefiniowanej klasy (Item) jak i użycia stdClass() są porównywalne.

A jak przedstawia się proces pobierania wartości z tych trzech utworzonych struktur danych?
Acquire from Array     1.7432529926
Acquire from Object     2.1544189453
Acquire from Item     2.3280811310
Znów nie ma większych zaskoczeń- używanie tablic okazuje się o wiele szybsze niż zaprzęganie klas. Co ciekawe pobieranie danych z instancji utworzonych za pomocą stdClass() okazuje się szybsze (choć nieznacznie) od pobierania wartości atrybutów z klas zdefiniowanych przez użytkownika.

Wnioski
To co chciałem pokazać w tym wpisie to to, że nie warto bezkrytycznie podchodzić do obecnych "trendów". Jeśli potrzebujemy maksymalnie wydajnego sposobu na przekazanie zestawu danych z jednego miejsca w programie do drugiego - to definiowanie klas mija się z celem. Oczywiście jeśli chcemy by nasz kod był maksymalnie przenośny to warto (a wręcz należy) użyć klas - ale tych definiowanych przez użytkownika. Wszak po to tworzymy definicję struktury klasy aby móc dołączyć do niej metody, a w przypadku używania stdClass otrzymujemy (pod względem czystej funkcjonalności) tablicę, która w dodatku działa wolniej od tej "standardowo" tworzonej za pomocą Array().

Plik z testami do pobrania z chomika (link).

czwartek, 29 września 2011

unexpected T_PAAMAYIM_NEKUDOTAYIM

Wpis ku pamięci :)

Komunikat ten (unexpected T_PAAMAYIM_NEKUDOTAYIM) otrzymujemy, gdy używamy :: w kontekście, którego PHP nie obsługuje (out of scope), czyli np. odwołujemy się do zwykłych metod, a nie do metod statycznych. Co ciekawe błąd ten wyskakuje w PHP < 5.3 również gdy wywołujemy istniejącą metodę statyczną przy użyciu zmiennych:
$className::$methodName();
Problem ten oczywiście nie istnieje w PHP >= 5.3, jednak przy starszych wersjach wciąż można natknąć się na tego dziwoląga :)

Alternatywą dla tego rozwiązania jest użycie funkcji call_user_func(), ta jednak działa wolniej od takiego bezpośredniego wywołania:
call_user_func($class."::".$method);
Dla porównania sprawdziłem również czas wykonywania zwykłych (niestatycznych) metod:
class::method				    0.0067858696
call_user_func				    0.0170218945
call_user_func (array)			    0.0149610043
class->method				    0.0044798851
call_user_func (array non-static)	    0.0125558376
Kod z testu do pobrania z chomika (link).

poniedziałek, 26 września 2011

Dzielenie ciągów przy pomocy wielu separatorów

Czasem zdarza się tak, że musimy podzielić nasz ciąg tekstowy w wyznaczonych miejscach na kilka mniejszych. W tej sytuacji funkcja explode() sprawdza się wręcz znakomicie. Problem jednak pojawia się gdy chcemy dzielić za pomocą więcej niż jednego separatora, gdyż samo explode() obsługuje jedynie separator w postaci pojedynczego "stringu". Na poniższych przykładach pokazałem różne sposoby rozbicia ciągu przy użyciu trzech separatorów: & ? /.

Rozwiązania
Najtrywialniejszym sposobem na poprawne rozbicie wg kilku separatorów byłoby wywołanie ponownie explode() dla każdego elementu wynikającego z pierwszego użycia tej funkcji, jednak ten sposób aż bije po oczach swym prostactwem więc nawet nie będę bawił się w jego poprawną implementację, a użyję jedynie takiego (dużego i nierealizującego zadania) przybliżenia:
for ($i = 0; $i < 100000; $i++)
{
	$temp = explode ( "/" , $string);
	foreach($temp as $elem)
	{
		$e = explode("?", $elem);
		$e = explode("&", $elem);
	}
}

Kolejnym pomysłem jest iteracyjne podejście - używanie na przemian explode() i implode() w celu stworzenia pojedynczej tablicy z wynikiem.
for ($i = 0; $i < 100000; $i++)
{
	$temp = explode ( "/" , $string);
	$temp = explode("&", implode("&", $temp));
	$temp = explode("?", implode("?", $temp));
}

Skoro już postanowiliśmy iść w stronę podmian to warto by również rozważyć wielokrotne użycie str_replace().
for ($i = 0; $i < 100000; $i++)
{
	$temp = str_replace("&", "/", $string);
	$temp = str_replace("?", "/", $temp);
	$temp = explode ( "/" , $temp);
}

Nim przejdziemy do wyrażeń regularnych przytoczę tu jeszcze jeden przykład, który niestety działa jedynie w przypadku separatorów będących pojedynczymi znakami. Co ciekawe mimo, iż wykorzystanie funkcji strtok() wydaje się być potwornie nieefektywne to wyniki zbliżone są do naprzemiennego wywoływania funkcji explode() i implode().

for ($i = 0; $i < 100000; $i++)
{
	$tok = strtok($string, "/&?");
	$temp = array();
	while ($tok !== false)
	{
		$temp[] = $tok;
		$tok = strtok("/&?");
	}
	unset($tok);
	strtok('', '');
}

Ale czy mamy ograniczać się jedynie do takich kombinowanych sposobów? Może jest jedna "magiczna" funkcja, która zrobi to o czym myślimy? Ano jest i wykorzystuje magię wyrażeń regularnych, a zwie się ona preg_split().
for ($i = 0; $i < 100000; $i++)
{
	$temp = preg_split("/[\/&?]/",$string);
}

Tutaj jednak warto zwrócić uwagę na fakt, że jeśli naszym separatorem jest znak wielobajtowy to funkcja ta będzie generować dodatkowe puste elementy w wynikowej tablicy (jeden dodatkowy element na każde wystąpienie separatora). Sytuacja ta nie ma jednak miejsca jeśli separatorem nie jest znak a ciąg znaków, nawet jeśli wszystkie zawarte w nim elementy są znakami wielobajtowymi.
Tak czy inaczej remedium na ten problem jest funkcja mb_split():
for ($i = 0; $i < 100000; $i++)
{
	$temp = mb_split("[\/&?]",$string);
}

Ostatnim przytoczonym tu przykładem będzie połączenie wyrażeń regularnych wykorzystanych w funkcji preg_replace() wraz z pojedynczym wywołaniem explode():
for ($i = 0; $i < 100000; $i++)
{
	$temp = preg_replace("/[&?]/","/",$string);
	$temp = explode("/", $temp);
}

Testy wydajności
Jak widać istnieje wiele możliwości wykonania zadania dzielenia ciągu z użyciem wielu równorzędnych separatorów, ale które rozwiązanie jest najefektywniejsze? Popatrzmy na wyniki:

single explode:		    0.1573359966
multiple explode:	    1.0633759499
explode & implode:	    0.7556319237
replace & explode:	    0.4618999958
strtok:			    0.6737470627
preg_split:		    0.9344220161
mb_split:		    1.1594960690
preg_replace & explode:	    1.1028780937

Dla separatorów składających się również z kilku znaków otrzymujemy takie wyniki:

single explode:		    0.1560928822
multiple explode:	    1.3294098377
explode & implode:	    1.1651928425
replace & explode:	    1.0913951397
preg_split:		    1.8065340519
mb_split:		    4.9289889336
preg_replace & explode:	    1.8077979088

Jak widać wszystkie czasy wzrosły, przy czym wzrost mb_split() wynika w dużej mierze z używania wielobajtowych znaków.
Jeśli jednak zostaniemy przy tych trzech jedno-znakowych separatorach, a jedynie zwiększymy rozmiar badanego ciągu otrzymamy takie oto wyniki (10x mniej iteracji pętli testujących):

single explode:		    0.2046608925
multiple explode:	    1.8381199837
explode & implode:	    1.0454828739
replace & explode:	    0.5224039555
strtok:			    1.0597200394
preg_split:		    1.8259940147
mb_split:		    2.1428530216
preg_replace & explode:	    1.8069810867

Jeśli dodatkowo zamiast trzech separatorów użyjemy 5 będziemy mieć takie oto wyniki:

single explode:		    0.2125821114
multiple explode:	    4.1121268272
explode & implode:	    3.3804459572
replace & explode:	    1.6378400326
strtok:			    3.4061551094
preg_split:		    2.7953231335
mb_split:		    6.4811940193
preg_replace & explode:	    3.0788750648

Wnioski
Jak widać wydajność metod różni się w zależności tak od długości badanych ciągów, jak i ilości oraz postaci separatorów. Porównując wyniki możemy jednak dojść do wniosku, że najlepszym rozwiązaniem jest użycie duetu str_replace() wraz z explode(), który to we wszystkich testach okazał się najszybszym rozwiązaniem. Pozostałe metody są w zasadzie porównywalne, choć stosunek ich wydajności różni się w zależności od parametrów zadania (długość ciągu i postać separatorów). Pomimo to możemy jasno stwierdzić, że wielokrotne używanie explode() (multiple explode) jest całkowicie nieefektywne w porównaniu do pozostałych metod (zwłaszcza, że nie zaimplementowano tej funkcji w całości). Podobnie sytuacja wygląda w przypadku mb_split(), którą używać należałoby jedynie w przypadku skomplikowanych podziałów, a nie prostego dzielenia wg podanych słów.

Wykorzystany kod do testowania można pobrać z mojego chomika (link).

niedziela, 18 września 2011

Co zmienia var?

Tematem tego posta będzie znaczenie słówka "var" występującego w JavaScript. Otóż wielu z programistów traktuje po macoszemu deklarację zmiennej, bo wszak w większości przypadków dodanie tych 3 literek nic nie zmienia. Jednak są takie momenty, gdy to słowo kluczowe decyduje o tym, czy skrypt będzie działał poprawnie, czy też będzie skrycie psuł nam krew.
Generalnie problemy z inicjacją zmiennych w JavaScript pojawiają się gdy zaczynamy tworzyć równolegle wykonywane fragmenty kodu, czyli używamy window.setTimeout() oraz window.setInterval(). Szkopuł tkwi w tym, że zmienna utworzona bez użycia "var" zachowuje się jak zmienna statyczna, a dopiero dodanie wspomnianego słówka powoduje, że będzie się ona zachowywać jak normalna zmienna lokalna.

Może jednak pokażę na przykładzie o co mi chodzi :) Przykład będzie trywialny, ot mamy dwa guziki z liczbami, z których pierwszy po kliknięciu ma dodawać "0.1" co 1/10 sekundy, a drugi ma "0.1" odejmować. Oczywiście te 0.1 nie mogą być na sztywno zapisane wewnątrz funkcji.

<script type="text/javascript">

function __countNoVar(node, finalValue)
{
delta = finalValue - Number(node.innerHTML);
delta = Number(Number(delta / 10 ).toFixed(1));

var intervalIdent = self.setInterval(function(){
	    node.innerHTML = (Number(node.innerHTML)+delta).toFixed(1);
	},100);
}

</script>
<button onclick="__countNoVar(this, 1);">0</button>
<button onclick="__countNoVar(this, -1);">0</button>
Dodaje: Odejmuje:

Jak widać jeśli pojedynczo wciśniemy guziki to działają bez problemu, ale w momencie wciśnięcia drugiego buttona nagle również pierwszy zaczyna liczyć w tym samym "kierunku" co ostatnio wciśnięty. Dzieje się tak właśnie z powodu braku "var" przed deklaracją zmiennej "delta", czego wynikiem jest używanie nowej wartości tej zmiennej tak w ostatnio utworzonym "wątku" jak i we wszystkich poprzednich utworzonych za pomocą funkcji "__countNoVar".

Wersja z "varem", która w wyglądzie różni się dosłownie 3 literami działa już jednak poprawnie:
<script type="text/javascript">

function __countWithVar(node, finalValue)
{
var delta = finalValue - Number(node.innerHTML);
delta = Number(Number(delta / 10 ).toFixed(1));

var intervalIdent = self.setInterval(function(){
	    node.innerHTML = (Number(node.innerHTML)+delta).toFixed(1);
	},100);
}

</script>
<button onclick="__countWithVar(this, 1);">0</button>
<button onclick="__countWithVar(this, -1);">0</button>
Dodaje: Odejmuje: