PHP: černá magie optimalizace
Když jsem včera psal o optimalizaci PHP skriptů pro rychlost, dostalo se mi v komentářích ostré kritiky. Zdálo se totiž, že řeším podružné detaily. Že tomu tak není, doufám dostatečně demonstrovala droboučká optimalizace jednoho konkrétního a smysluplného skriptu. Podařilo se čas potřebný k vykonání srazit z původních 30 minut na pouhé tři sekundy!
Za drastickým zrychlením stojí opět vhodné užití referencí a přiřazování. V minulém článku jsem leccos naznačil, dnes prozradím celé pozadí. Nevěřte však bulvárnímu titulku, nejde o žádnou černou magii. Znova opakuji, stačí jen pochopit, jak PHP funguje uvnitř. Ale nebojte se, není to nic složitého.
Reference counting do hloubky
Jádro PHP si v paměti ukládá jména proměnných odděleně od jejich
hodnot. Bezejmennou hodnotu popisuje struktura zval.
Ta nese kromě surových dat také informaci o typu (boolean, string, …) a
ještě dvě položky refcount a is_ref. Ano,
refcount je právě počítadlo pro již zmíněný reference
counting.
$abc = 'La Trine';
Co dělá skutečně tento kód? Vytvoří v paměti novou hodnotu
zval, jejíž datová složka ponese 8 znaků La
Trine a typ bude indikovat string. Zároveň se do tzv. tabulky
proměnných přidá nový záznam abc a bude odkazovat na tuto
hodnotu zval.
Ještě něco. Ve struktuře zval inicializujeme počítadlo
refcount na jedničku, protože existuje právě jedna proměnná
($abc), která na něj ukazuje.
// 10MB velký řetězec
$sA = str_repeat(' ', 1e7);
$sB = $sA;
Jak si PHP poradí s přiřazením na druhém řádku? Samozřejmě,
vytvoří v tabulce proměnných nový záznam sB. Teď pozor –
záznam bude odkazovat na stejný zval, na jaký odkazuje již
záznam sA. A zároveň inkrementuje počítadlo
refcount.
Což je skvělé! Není potřeba zabrat dalších 10MB paměti, nedojde k časově náročnému kopírování dat. Operace proběhne bleskově.
Nojo, ale z pohledu PHP programátora jde o dvě různé proměnné. Co když jednu změním?
$sB .= 'the end';
Žádný strach, o všechno je postaráno. Když vznikne požadavek na
zápis do proměnné, PHP se podívá na odkazovaný zval a
zkontroluje počítadlo refcount. Pokud refcount >
1, tak celou hodnotu zval zduplikuje a nechá
sB odkazovat na tuto kopii. Samozřejmě ještě sníží
refcount u původního zval.
Pro úplnost ještě dodám, že konstrukce unset($sB) zruší
záznam sB v tabulce proměnných a dekrementuje příslušný
refcount. A jakmile refcount padne k nule,
z paměti uvolní strukturu zval – už na ni totiž žádná
proměnná neodkazuje.
Klasické reference, penetrované do hloubky
Zatím je vše jasné? Tak pojďme na druhou lekci a ukažme si, jak jádro nakládá s klasickými referencemi.
$a = 'La Trine';
$b = & $a;
Jakým způsobem PHP vykoná první řádek, to už dobře víte. Co se ale
děje pod kapotou v případě řádku druhého? Když jsem popisoval strukturu
zval, zmínil jsem se o is_ref. Jde o boolean,
indikující, zda hodnota zval je či není referencí. A právě
teď přichází jeho patnáct minut slávy.
PHP vytvoří proměnnou $b úplně stejně, jako v příkladu
bez použití reference, jen navíc nastaví is_ref na TRUE.
V tuto chvíli se proměnné $a i $b (obě!)
stávají referencemi, tak jak je známe.
Podstatný rozdíl přijde v okamžiku, kdy se pokusíme jednu proměnnou
změnit. Protože is_ref je TRUE, vynechá se test na
refcount a s ním celý mechanismus duplikování. Prostě se
rovnou změní společná hodnota zval. I když… ale k tomu se
hned dostaneme.
Můžeme vytvářet další reference $xyz = & $a, rušit je
unset($b), princip zůstává stejný. Jádro pracuje s tabulkou
proměnných a aktualizuje počítadlo refcount.
Stále všechno srozumitelné? Jestli náhodou ne, zkuste si přečíst článek ještě jednou a pomaleji. Nyní totiž bude potřeba maximální soustředěnost.
Půvab pomalu mizí
Zkuste se zamyslet nad tím, jak PHP vykoná následující kód:
$a = 'La Trine';
$b = & $a;
$c = $a;
Proměnné $a a $c odkazují na tentýž
zval, mající vynulovaný is_ref. Proměnné
$a a $b ale zase potřebují mít is_ref
nastavený. To lze vyřešit leda tak, že budeme mít dvě hodnoty
zval.
Jinými slovy, řádek č. 3 musí duplikovat hodnotu
zval:

Výše uvedený algoritmus pro vytváření nových proměnných je proto
potřeba doplnit o podmínku: pokud je refcount > 1 a
„neodpovídá“ požadovaný is_ref, tak holt duplikuj a
nekoukej, co kde lítá.
Obdobně, duplikovat se bude i v tomto případě:
$a = 'I love La Trine :-)';
$b = $a
$c = & $a;
Vidíte to? Vytváření reference duplikuje hodnotu proměnné. Kopie,
s nastaveným is_ref, bude odkazována proměnnými
$a a $c (jen pro úplnost, refcount =
2).
Možná si teď říkáte, co to je za šílenost, proč je jádro PHP tak špatně navrženo? Věřte mi, není. Jde o běžný problém sdíleného vs. exkluzivního přístupu, jen se nazývá jinak. Dalo by se tomu vyhnout, ale změna návrhu by natolik zkomplikovala práci s proměnnými, že by byla v globálu zcela kontraproduktivní.
Optimalizace Johnova skriptu
Konečně můžu vysvětlit trik stojící za optimalizací Johnova skriptu.
...
$arr = &$this->table;
foreach($ngram as $token) {
// if(!array_key_exists($token, $arr)) {
// $arr[$token] = array();
// }
$arr = &$arr[$token];
}
...
Mohlo by se zdát, že za úspěchem stojí odstranění funkce
array_key_exists, která je nejspíš tak šíleně pomalá, až to
celé stáhla ke dnu. No schválně, kdo si to myslel, ať mi pošle Nutellu
Kdepak. Pes je zakopán jinde.
Teď už víte, že předávaná proměnná $arr odkazuje
zval, mající nastavený bit is_ref a počítadlo
refcount = 2 (hodnota je odkazována z $arr a
zároveň samotným prvkem pole). Co je klíčové, tak že tento
zval pojímá obrovské pole.
Při přiřazení do funkce array_key_exists se stane
nevyhnutelné – zval se musí zduplikovat. Což doslova zatáhne
brzdu jedoucímu skriptu. Kdyby se volala třeba funkce key(),
která parametr přebírá referencí, nebo kdybychom porušili zapovězenou
syntax Call-time
pass-by-reference a argument vnutili referencí
array_key_exists($token, &$arr), tak ke kopírování nedojde.
A skript se 600× zrychlí.
Bílá magie optimalizace
Mým cílem bylo smést pověry a mýty kolem referencí. Že jsou něco jako ukazatele, že zrychlí kód. Pravda je taková, že všechny proměnné jsou de facto ukazatelé. Jen se liší způsob, jak s nimi jádro PHP pracuje.
Pokud tyto principy znáte, můžete je využit ve svůj prospěch (zdůrazňuji slovo „můžete“). Můžete efektivněji nakládat s řetězci nebo poli. Jakmile vám přejdou do krve, budete je využívat zcela podvědomě, stane se z nich Coding Standard.
Komentáře
» přidat
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.

#1 johno http://johno.jsmf.net/ nový
Ja by som len doplnil, že tvrdenie
nie je tak úplne presné. Ono totiž záleží hlavne na tom, aké veľké to kopírované pole je. Môže to byť aj o dosť menej aj o dosť viac.
Preto to nakoniec mne dávalo iné výsledky ako tebe. Na dôvode spomalenia to však nič nemení.
#2 BlackSUN nový
Jestli jsem to tedy dobře pochopil v sekci Půvab pomalu mizí, po provedení kódu bude odkazovat $a a $c na zval s nastavenym is_ref a na puvodni zval bude odkazovat jenom $b a refcount bude mit 1. Protoze jinak by $a odkazovalo na dve zval, coz by asi jit nemelo. Chapu to spravne?
#3 Michal Hantl http://hantl.info nový
Dík za článek. BTW dgxi, proč stále programuješ v PHP? Nebylo by přínosnější pro webové programátory přejít na Javu?
Předem dík za odpověď.
#4 Petr Stříbný http://stribny.name nový
#3 Michal Hantl: To na něj raději nezkoušej, už se tady vedla diskuse o ASP.NET, ještě aby tady byl flame o Javě :)
dgx: Fakt užitečné články, díky za ně (i když od PHP pomalu odcházím..)
#5 slavista nový
Trochu je to vysvětleno i zde (graficky):
http://talks.php.net/…s-ffm2005/24
(další screen šipkou vpravo)
Zde jsou další prezentace (možná znáte, možná ne):
http://talks.php.net/Internals
#6 Michal Hantl http://hantl.info nový
Nechci generalizovat, ani porovnávat. Jen mě zajímá proč dgx osobně preferuje (preferuje-li) PHP. Pakliže ne, zajímá mě proč v něm dělá.
#7 Borek http://www.borber.com/ nový
Davide, mohl bys mi jako úplnému laikovi vysvětlit, proč je atribut
is_refdržen na straně hodnoty a ne na straně proměnné? Pokud by seis_refuchovávalo v tabulce proměnných, tak mi na první pohled mi připadá, že by se časově náročná kopie dat dala odložit na pozdější dobu.#8 David Grudl http://davidgrudl.com nový
#2 BlackSUN: Přesně tak
#3 Michal Hantl: rád zkouším různé jazyky a teď mi zrovna frčí PHP. Nic jiného v tom není.
#5 slavista: to je hezký! Chtěl jsem taky doplnit vševysvětlující obrázek, ale teď už nemusím. Ale dám ho tam. (pozn: pokračuje se šipkou doprava)
#7 Borek: V tom případě bys narazil na problém, že při duplikování by se musela projít tabulka (nebo tabulky?) proměnných, zjistit, kdo na
zvalodkazuje referencí a tyto odkazy aktualizovat. Složitost by pak byla o(x).#9 Radek Hulán http://hulan.cz/ nový
Pěknej článek, Davide
Budu muset změnit svůj pohled na PHP, 600× rozdíl
bych opravdu netušil, a je navíc zajímavé vidět a vědět, kdy a jak
nastává..
#10 Petr http://melodie.na-mobil.cz nový
Hm, když pominu fakt, že řici, že něco je x krat rychlejsi je spatne samo o sobe (rozhodne tyto algoritmy nejsou primitivne linearni), tak musim rici, ze clanek poukazuje na zajimavou vlastnost jadra. Asi zacnu zkouset jak je to v jinych jazycich, kde to muze byt zcela uplne jinak. Zalezi jen na vnitrnich principech kompilatoru, to je hezke :)
#11 Bohdan nový
#8 David Grudl: Jedině oba přístupy spojit a ukládat
is_refv tabulce promněných a dvě počítadla vzval.Rozhodně když napíšu
$b = & $a;tak předpokládám že se nic kopírovat nebude. Kolik procent lidí píšících v php asi tak ví jak tahle (určitě užitečná) šílenost funguje. Když skript, který vypadá na první pohled celkem v pořádku, je 600krát pomalejší než by mohl být kdyby autoři php zvolili jiný přístup…Jistě, když už člověk v php programuje, je lepší vědět co se kdy děje do nejmenšího detailu. Ale mám pocit že php má podobných záhadných vlastností trochu moc.
#12 martinpav nový
http://www.derickrethans.nl/…-article.pdf#…
#13 Hever nový
Pěkná věc, pěkně popsaná, příště budu víc přemýšlet u komentářů a nutelu su nechám pro sebe.
Můžu za běhu nějak sledovat hodnoty is_ref a refcount? Ono místo, kde se zapisují jména proměnných má přezdívku $GLOBALS? Je pole uloženo jednom zvalu, nebo každá část zvlášt? (Když referencuji nějakou jeho část někde, co můžu očekávat..)
Vidím, že je potřeba víc sledovat, jak která funkce s proměnnou nakládá (a třeba i global proměnnou referencuje).
#14 David Grudl http://davidgrudl.com nový
#13 Hever: pokud ale proměnná nese skalární typ, tak to nemá smysl řešit – tady jde především o velká pole nebo dlouhé řetězce.
Hodnoty is_ref a refcount můžeš sledovat pomocí fce debug_zval_dump(), ale tady bohužel platí poučka kvantové fyziky, že pozorování ovlivňuje výsledky (předání parametru funkci jej pozmění). Takže je to na houby a raději zkus xdebug_debug_zval.
Jinak pole je interně jeden zval obsahující pole klíčů, které odkazují na další zval.
#15 paranoiq nový
na reference pozor také u globálních proměnných ve funkcích! při deklaraci global $var; ve funkci není $var ‚zviditelněna‘, ale je na ni vytvořena stejně pojmenovaná lokální reference. zavoláte-li poté unset($var), globální proměnná zůstane nedotčena.