Lookup Table în PHP

Problema implementării unui lookup table pare uneori simplă. În cazul în care se implementează ca un array indexat, iar obținerea unei valori se face prin metoda dă-mi valoarea Y de la cheia X, atunci problemele de performanță nu intervin.

M-am lovit de altă bubă, și anume implementarea unui lookup table simplu bazat pe căutare liniară construit pe baza unui pre-fetch. Ideea este simplu de implementat, și deși procesul respectiv nu rulează prea des, numărul valorilor distincte complică problema destul de mult. Într-un lookup table de 70.000 – 80.000 valori căutarea unui număr mai mare de valori durează peste jumătate de oră (nu știu exact, am oprit benchmark-ul după 30 minute) cu procesorul (sau un core) la ~100% load, pe când restul operațiilor se execută în secunde. Cam multicel pentru o căutare ce ar trebui să returneze TRUE / FALSE. Faptul că numărul de căutări este mai mare decât lookup table-ul în sine mai bate un cui în coșciugul metodei de mai sus. Se mai adaugă și faptul că deși în teorie valorie ar trebui să fie unice, datele din pre-fetch nu sunt neapărat validate în acest sens.

Pre-fetch – pseudo implementare

$haystack = array();
while($value = get_data())
{
	$haystack[] = $value;
}

Ideea este simplă. Stiva $haystack va conține toate valorile, inclusiv cele duplicat. Mă rog, pre-fetch-ul implementează ideea de “stivă” pentru că auto-indexarea cu [] are aceeași funcționalitate ca și array_push(), dar câștigă puțin la viteza de inserare a datelor. Durează câteva secunde, nu este o operație foarte complexă. De aici începe greul.

Căutare – implementare uzuală

Există tentația de a face căutarea pe baza lui in_array() sau array_search(). Ambele sunt aproximativ la fel de rapide (in_array() este cu maxim 1% mai lentă). Problema principală este căutarea liniară, fără indexare.

Dau chiar exemplele din codul meu de benchmark:

	public function bench_in_array($needle)
	{
		return in_array($needle, $this->haystack);
	}
 
	public function bench_array_search($needle)
	{
		if (array_search($needle, $this->haystack) === FALSE)
			return FALSE;
		return TRUE;
	}

Căutare – soluția optimă

Ambele soluții anterioare sunt aproximativ la fel de rapide, dar aproximativ la fel de lente. Interesul este dacă există o valoare. Duplicatele doar încurcă.

$haystack = array_flip($haystack);

Inversează valoarea cu cheile dintr-un array. Valorile duplicat dispar din moment ce un array (mă rog, devine ordered map) nu poate avea chei duplicat. Se pot aplica alte două metode distincte. Dau din nou exemplu din codul meu de benchmark:

	public function bench_array_flip_isset($needle)
	{
		return isset($this->flipped_haystack[$needle]);
	}
 
	public function bench_array_flip_array_key_exists($needle)
	{
		return array_key_exists($needle, $this->flipped_haystack);
	}

Pentru o listă de $needle scurtă (1% din $haystack, valori 100% valide), sunt aproximativ echivalente. Pentru un input 50% valid de dimensiunea $haystack, array_key_exists() este cu aproximativ 80% – 100% mai lent. Dar acum urmează partea frumoasă: căutarea se face indexat. Metoda este de cel puțin 2000 ori mai rapidă decât căutarea cu in_array() / array_search(). Adică în mod uzual căutarea de mai devreme de peste jumatate de oră durează aproximativ sub 3 secunde cu isset() și sub 5 secunde cu array_key_search(). Metodă testată pe un lookup table de 100.000 valori ce mi-a cam ucis browserul. Chrome a crăpat, Firefox a încărcat output-ul de la Codebench în vreo 3.5 GiB RAM. Atașez și imagini ce stau dovadă atrocității de mai devreme.