Na navigaci | Klávesové zkratky

Translate to English… Ins Deutsche übersetzen…

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

  1. johno http://johno.jsmf.net/ #1

    avatar

    Ja by som len doplnil, že tvrdenie

    A skript se 600× zrychlí.

    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í.

    před 10 lety
  2. BlackSUN #2

    avatar

    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?

    před 10 lety | reagoval [8] David Grudl
  3. Michal Hantl http://hantl.info #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ěď.

    před 10 lety | reagoval [4] Petr Stříbný [8] David Grudl
  4. Petr Stříbný http://stribny.name #4

    #3 Michale Hantle, 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..)

    před 10 lety
  5. slavista #5

    avatar

    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

    před 10 lety | reagoval [8] David Grudl
  6. Michal Hantl http://hantl.info #6

    ještě aby tady byl flame o Javě :)

    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á.

    před 10 lety
  7. Borek http://www.borber.com/ #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 se is_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.

    před 10 lety | reagoval [8] David Grudl
  8. David Grudl http://davidgrudl.com #8

    avatar

    #2 BlackSUNe, Přesně tak

    #3 Michale Hantle, rád zkouším různé jazyky a teď mi zrovna frčí PHP. Nic jiného v tom není.

    #5 slavisto, 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 Borku, 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).

    před 10 lety | reagoval [11] Bohdan
  9. Radek Hulán http://hulan.cz/ #9

    avatar

    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á..

    před 10 lety
  10. Petr http://melodie.na-mobil.cz #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 :)

    před 10 lety
  11. Bohdan #11

    avatar

    #8 Davide Grudle, Jedině oba přístupy spojit a ukládat is_ref v tabulce promněných a dvě počítadla v zval.

    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.

    před 10 lety
  12. martinpav #12

    před 10 lety
  13. 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).

    před 10 lety | reagoval [14] David Grudl
  14. David Grudl http://davidgrudl.com #14

    avatar

    #13 Hevere, 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.

    před 10 lety
  15. paranoiq #15

    avatar

    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.

    před 10 lety

Tento článek byl uzavřen. Už není možné k němu přidávat komentáře.