Nedávno se mi podařilo zrychlit jeden PHP skript na setinu původního času. A stačilo změnit jen pár znaků ve zdrojovém kódu. Jak je to možné? Za drastickým zrychlením stojí vhodné užití referencí a přiřazování. Prozradím vám, jak to funguje. 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 skriptu
Konečně můžu vysvětlit trik stojící za optimalizací zmíněného skriptu. Byl v něm následující kód:
...
$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
johno #1
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í.
BlackSUN #2
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?
Michal Hantl #3
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ěď.
Petr Stříbný #4
#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..)
slavista #5
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
Michal Hantl #6
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á.
Borek #7
Davide, mohl bys mi jako úplnému laikovi vysvětlit, proč je atribut
is_ref
držen na straně hodnoty a ne na straně proměnné? Pokud by seis_ref
uchová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.David Grudl #8
#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
zval
odkazuje referencí a tyto odkazy aktualizovat. Složitost by pak byla o(x).Radek Hulán #9
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á..
Petr #10
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 :)
Bohdan #11
#8 David Grudl, Jedině oba přístupy spojit a ukládat
is_ref
v 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.
martinpav #12
https://web.archive.org/…-article.pdf
Hever #13
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).
David Grudl #14
#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.
paranoiq #15
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.
Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.