0xDEADBEEF

RSS odkazy

PHP by byl lepší jazyk, kdyby byl napsaný v Rustu

Ne nutně v Rustu, ale v jakémkoli jazyce, který má dobré nástroje pro metaprogramování.

Jeden z problémů, který stojí v cestě zrychlování PHP, je fakt, že interní funkce jsou napsané v C. Ty pro JIT představují neprůhledný blob a JIT nemůže optimalizovat napříč voláním do nich. Může znát statické typy proměnných a mít je v registrech, ale jakmile narazí na interní funkci, musí tohle všechno zahodit, zrekonstruovat stav vyžadovaný interpretrem, alokovat stack frame (opkód INIT_FCALL, funkce _zend_vm_stack_push_call_frame), proměnné zkopírovat z registrů a zrekonstruovat zvaly, které úspěšně odoptimalizoval (SEND_VAL) a pak zavolat samotnou interní funkci (DO_ICALL, proměnná _zend_function.internal_function.handler).

Funkce jako první začne parsovat argumenty, aby ověřila jejich typy a nastrká je zas do registrů a tohle všechno je zbytečná práce a promrhaný čas. Není to moc, jen pár nanosekund, ale je to pár nanosekund pro každé volání každé interní funkce a to se rychle nastřádá.

Definitivní způsob jak odstranit tuhle překážku je přepsat interní funkce z C do PHP nebo do meta-jazyka (v duchu RPythonu), aby skrz něj JIT viděl a mohl je optimalizovat v kontextu jako jakýkoli jiný kus PHP kódu.

To je ale obrovský krok, který zasáhne velkou část tří milionů řádek PHP zdrojáků.

Napůl cesty nás může dostat, když zredukujeme cenu volání. Bylo by super, kdyby v případě, kdy si je JIT naprosto jistý, že typy argumentů pasují, funkci zavolal přímo a obešel taneček, který je nutný jen pro interpretr. Funkce umíme volat přímo, System V ABI je věc, která existuje v našem vesmíru. Problém je jak přinutit PHP, aby tak činilo, pokud možno s nejmenší možnou námahou.

A tady do hry vstupuje ta myšlenka, že kdyby PHP bylo napsané v expresivnějším jazyce, tohle by nebyl problém. Já to vím nejlépe, protože aniž bych to plánoval, jsem tento problém vyřešil.

Nejdřív ale musím vysvětlit jak v PHP vypadá interní funkce.

Představte si, že mám funkci str_popcount, která spočítá počet nastavených bitů ve stringu a chci ji exportovat do PHP. Rozšíření je (v duchu monstrozity PHP) nepřehledná změť mnoha souborů, některých automaticky generovaných skriptem phpize mezi nimiž se můj užitečný kód zcela ztrácí.

Tělo funkce může vypadat takhle:

PHP_FUNCTION(str_popcount)
{
  zend_string *data;

  ZEND_PARSE_PARAMETERS_START(1, 1)
    Z_PARAM_STR(data)
  ZEND_PARSE_PARAMETERS_END();

  zend_long result = str_popcount(ZSTR_VAL(data), ZSTR_LEN(data));

  RETVAL_LONG(result);
}

Deklaruje proměnné, naparsuje parametry, provede samotný výpočet a pak vrátí výsledek. Parsování parametrů bych se rád zbavil.

Může to vypadat přehledně, ale to jen proto, že makra skrývají děs PHP zdrojáků. Po expanzi funkce vypadá monstrózně, jako zlý sen, který nikdy neskončí.

void zif_str_popcount(zend_execute_data *execute_data, zval *return_value) {
  zend_string *data;

  do {
    const int _flags = (0);
    uint32_t _min_num_args = (1);
    uint32_t _max_num_args = (uint32_t) (1);
    uint32_t _num_args = (execute_data)->This.u2.num_args
    uint32_t _i = 0;
    zval *_real_arg, *_arg = NULL;
    zend_expected_type _expected_type = Z_EXPECTED_LONG;
    char *_error = NULL;
    bool _dummy = 0;
    bool _optional = 0;
    int _error_code = ZPP_ERROR_OK;
    ((void)_i);
    ((void)_real_arg);
    ((void)_arg);
    ((void)_expected_type);
    ((void)_error);
    ((void)_optional);
    ((void)_dummy);

    do {
      if (UNEXPECTED(_num_args < _min_num_args) ||
          UNEXPECTED(_num_args > _max_num_args)) {
        if (!(_flags & ZEND_PARSE_PARAMS_QUIET)) {
          zend_wrong_parameters_count_error(_min_num_args, _max_num_args);
        }
        _error_code = ZPP_ERROR_FAILURE;
        break;
      }
      _real_arg = (((zval*)(execute_data)) + (((int)((sizeof(zend_execute_data) + sizeof(zval) - 1) / sizeof(zval))) + ((int)(((int)(0)) - 1))))


    ++_i;
    ZEND_ASSERT(_i <= _min_num_args || _optional==1);
    ZEND_ASSERT(_i >  _min_num_args || _optional==0);
    if (_optional) {
      if (UNEXPECTED(_i >_num_args)) break;
    }
    _real_arg++;
    _arg = _real_arg;
    if (0) {
      if (EXPECTED(Z_ISREF_P(_arg))) {
        _arg = Z_REFVAL_P(_arg);
      }
    }
    if (0) {
      do {
        zval *_zv = (_arg);
        ZEND_ASSERT(Z_TYPE_P(_zv) != IS_REFERENCE);
        if (Z_TYPE_P(_zv) == IS_ARRAY) {
          SEPARATE_ARRAY(_zv);
        }
      } while (0)
    }

    if (UNEXPECTED(!zend_parse_arg_str(_arg, &dest, 0, _i))) {
      _expected_type = 0 ? Z_EXPECTED_STRING_OR_NULL : Z_EXPECTED_STRING;
      _error_code = ZPP_ERROR_WRONG_ARG;
      break;
    }

      ZEND_ASSERT(_i == _max_num_args || _max_num_args == (uint32_t) -1);
    } while (0);
    if (UNEXPECTED(_error_code != ZPP_ERROR_OK)) {
      if (!(_flags & ZEND_PARSE_PARAMS_QUIET)) {
        zend_wrong_parameter_error(_error_code, _i, _error, _expected_type, _arg);
      }
      return;
    }
  } while (0);


  zend_long result = str_popcount((zval).value.str->val, (zval).value.str->len);

  do {
    zval *__z = (return_value);
    (*__z).value.lval = result;
    (*zval).u1.type_info = IS_LONG;
  } while (0);
}

Tenle text se pošle kompilátoru, který po zpropagování konstant většinu mrtvého kódu smaže. Zůstane jen nepříliš dlouhá sekvence instrukcí.

S tímhle se ale nedá nic moc dělat. C neumožňuje čistě bez přepisování všech funkcí separovat část parsující argumenty od užitečné práce funkce. Preprocesování prostou náhradou textu je přehnaně hrubý nástroj.

Potřebovali bychom se podívat na typy a podle nich generovat kód pro dvě verze této funkce – jedné takové jak je napsaná výše a druhé, které přeskočí parsování argumentů.

Před nějakou dobou jsem v jazyce D napsal nástroj pro snazší tvorbu PHP rozšíření. Z výkonových důvodů se mi hodilo exportovat několik nativních funkcí, ale nechtěl jsem zase projít utrpením s tvorbou rozšíření v C, jak to PHP gang zamýšlel. Jednou jsem to zkusil a dodnes mě to budí ze spaní. Ukázalo se, že vytvořit nový nástroj zcela od nuly je snazší než se prokousávat existující mizérií. Bylo třeba trochu reverzování, protože, jak je pro PHP obvyklé, mechanismus nahrávání rozšíření není nikde popsaný a detaily jsou zahrabané pod nekonečnými vrstvami maker.

Takhle vypadá kompletní rozšíření, co vystaví funkci str_popcount:

import phpmod;

ModuleEntry mod = {
  name: "popcount",
  version_: "1",
  functions: [
    func!(str_popcount, "str_popcount"),
    zend_function_entry(),
  ],
};

extern(C) ModuleEntry* get_module() {
  return &mod;
}

long str_popcount(const(ubyte)[] str) {
  import core.bitop;
  if (str.length == 0) return 0;
  long bits = 0;
  auto p   = str.ptr;
  auto end = str.ptr + str.length;
  for (; p < end - 7; p += 8) bits += popcnt(*cast(ulong*)p);
  for (; p < end;     p += 1) bits += popcnt(*p);
  return bits;
}

A když říkám kompletní, myslím jako že tohle je všechno: jeden soubor, co importuje modul phpmod, také jeden soubor. Dokonce jsem přidal implementaci samotné funkce, kterou jsem z PHP ukázky pro přehlednost vyhodil.

Všimněte si, že nikde nezmiňuji PHP typy. Argumentem není zend_string, ale pole bajtů. Funkce nenastavuje návratovou hodnotu přes RETVAL_LONG, ale jednoduše ji, jako v civilizovaném světě, vrátí. Mechanismus parsování argumentů a kód pro kontakt s PHP rozhraním je během kompilace na základě typů automaticky generovaný v templatované funkci func. Dokonce, když v nativním kódu vyhodím výjimku, ta je propagovaná napříč rozhraním a můžu ji odchytit na straně PHP.

Když interní funkce píšu přes tenhle mechanismus, nemusím generovat jen jednu funkci pro PHP ABI, ale i druhou nativní, co neparsuje argumenty, ale bere je, jak ji přijdou v registrech přesně podle diktátu System V ABI. Pointer na ní se uloží někam do internal_function a JIT ji může použít pro volání nativní rychlostí, když ví, že typy pasují. To všechno zcela automaticky beze změny jediného bajtu jediné interní funkce.

Takže ano, řekl bych, že by aspoň v některých ohledech PHP benefitovalo z lepšího jazyka, možná Rustu nebo něčeho jiného, co na frontě metaprogramování nabízí víc než textová makra.

Navíc kód by určitě vypadal lépe.


Dodatek: Na první pohled může vypadat, že buď dojde ke zpomalení nebo k duplikaci kódu. V jenom případě generovaná funkce parsující argumenty volá funkce s užitečnou prací. To si vyžádá call/ret pár. V druhém je tělo i wrapper inlinovány dohromady a kód je duplikován.

Nemusí tomu tak být.

Když mám:

int64_t __attribute__ ((noinline)) body(int64_t a) {
  return a;
}

int64_t parse_arguments(void* a) {
  int64_t i = *(int64_t*) a;
  if (i < 0) return 0;
  return body(i);
}

GCC vygeneruje následující binárku:

<body>:
    1100:	48 89 f8             	mov    %rdi,%rax
    1103:	c3                   	ret

<parse_arguments>:
    1110:	48 8b 3f             	mov    (%rdi),%rdi
    1113:	48 85 ff             	test   %rdi,%rdi
    1116:	78 08                	js     1120 <parse_arguments+0x10>
    1118:	eb e6                	jmp    1100 <body>
    111a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)
    1120:	31 c0                	xor    %eax,%eax
    1122:	c3                   	ret

parse_arguments přímo skočí do funkce body instrukcí jmpret na konci body se vrátí z funkce parse_arguments. Cena bude nižší, jeden nepodmíněný skok a fakt, že horký kód neleží na přímé cestě.

komentáře
4. 10. 2023DIY hash index v SQLite
18. 5. 2023PHP refcount jako optimalizace
21. 4. 2023Podmíněná inkrementace v x86
23. 1. 2023realloc, mremap & virtuální paměť
11. 12. 2022PHP FFI pro kompaktní data a parsování
5. 12. 2022Degenerované fulltext hledání v SQLite
8. 8. 2022Fingerprintování virtuálního procesoru
9. 6. 2022Protip: Eliminujte zbytečné maskování
11. 4. 2022Běží obě strany unixové pipe paralelně?
30. 1. 2022Propustnost dvoukanálových pamětí
10. 12. 2021Míchání intů a ulongů
7. 11. 2021Iterace směrem k nule
5. 11. 2021ulong -> float
20. 9. 2021Malé řazení ve vektoru - pcmpistrm
4. 9. 2021Paralelní souborové I/O přes io_uring
18. 8. 2021Osmibajtový hash
15. 8. 2021Dodatek k bitonic sortu a optimalizaci
19. 7. 2021Inserting 100 million rows in sqlite on old garbage hardware so fast it's not even funny
6. 7. 2021Bitonic sort
2. 6. 2021Scala 3 a klíčové slovo inline
25. 5. 2021Struct of arrays & array of structs
29. 4. 2021Optimalizace SplPriorityQueue
2. 4. 2021Struct of arrays & array of structs
12. 3. 2021Jak přesunout 1 až 16 bajtů do SIMD registru
10. 3. 2021Užitečné bash nástroje
16. 2. 2021Rychlosti a latence SSD disků
3. 2. 2021Proč je M1 tak rychlý?
16. 1. 2021Floating point triky
3. 1. 2021Asociativní pole v jazyce D
20. 12. 2020GC a granularita alokací v jazyce D
28. 11. 2020Top-k v PHP 7 a 8
12. 11. 2020Top-k
2. 11. 2020Průnik bitmap množin
27. 10. 2020Why I don't like Go
26. 10. 2020RSS in PHP
25. 10. 2020Proč nemám rád Go
15. 10. 2020Samooptimalizující se kolekce
9. 10. 2020Java, Scala, D, Javascript, PHP a regulární výrazy #8
26. 9. 2020Kešování na webu
22. 9. 2020Bez virtuální paměti
7. 9. 2020Rekurze, regexy a kombinátory parserů
29. 8. 2020Kolik registrů má x86?
10. 8. 2020Rychlý průnik množin a Jaccardův index přes SIMD instrukce
25. 7. 2020Parsování JSONu nemusí být (úplně) pomalé ani v Go
14. 7. 2020Java, Scala a regulární výrazy #7 - zero width positive lookbehind
2. 7. 2020Jazyk D, stringy, paměť a garbage collection
14. 6. 2020RSS v PHP
25. 4. 2020Parsování JSONu je neuvěřitelně pomalé, ale nemusí být
10. 3. 2020Jak dostat HTML z HTML DOMu v PHP
14. 1. 2020Scala: dočasné mutable a immutable mapy
20. 12. 2019Porovnání Javy a Scaly v porovnávání
25. 8. 2019Jak rychlá je reflexe na JVM?
21. 5. 2019Scala, mapy a čítače
13. 5. 2019Další novinky ve Scale 2.13
14. 4. 2019Parciální funkce ve Scale a dvojité vyhodnocování
10. 4. 2019Minimalistické zvýrazňování syntaxe
13. 3. 2019Jak rychlý je čas (v Javě)?
21. 2. 2019Really simple RSS
26. 1. 2019Java, Scala a regulární výrazy #6 - znovupoužití Matcher objektu
12. 1. 2019Poznámky ke slajdům k přednášce, kterou jsem nikdy nepřednášel
4. 1. 2019Minifikace HTML5
24. 12. 2018Java IO & NIO - jak nejrychleji načíst soubor z disku
3. 11. 2018Průnik množin ve Scale
23. 10. 2018Čím nahradit Scala.XML
11. 10. 2018Novinky kolekcí ve Scale 2.13
4. 8. 2018Co vlastně benchmarkujete?
28. 7. 2018Java, Scala a regulární výrazy #5 - posesivní regexy a StackOverflowError
26. 7. 2018EDGE, TRIPS a hon za vyšším ILP
24. 6. 2018Striktní a líné jazyky
21. 6. 2018Syntéza datových struktur a programů
9. 6. 2018ISPC, SPMD a SIMD
6. 4. 2018Konec Intelu, konec Moorova zákona, konec světa takového jaký známe
31. 1. 2018Meltdown, Spectre a branch prediction
28. 12. 2017Poznámky k výkonu
19. 12. 2017Java, Scala a regulární výrazy #4 - líné regexy a StackOverflowError
5. 12. 2017Java, Scala a regulární výrazy #3 - rychlost
27. 11. 2017Java, Scala a regulární výrazy #2 - rychlost
26. 11. 2017Java, Scala a regulární výrazy
21. 11. 2017Java - zostřování obrázků
7. 10. 2017Koherence cache a atomické operace
5. 10. 2017Nejrychlejší hashmapa pod sluncem
23. 9. 2017Dekódování x86 instrukcí
21. 9. 2017Energetická efektivita programovacích jazyků
17. 9. 2017B-stromy, persistence, copy-on-write a Btrfs
15. 9. 2017Rust a parsery
13. 9. 2017Velké stránky paměti
11. 9. 2017Deanonymizace agregovaných dat
9. 9. 2017Optimalizující kompilátor
7. 9. 2017Identifikace zašifrovaných videí
5. 9. 2017Heterogenní persistentní kolekce
3. 9. 2017JVM Anatomy Park
1. 9. 2017Datacentrum z mobilů
30. 8. 2017Propustnost pamětí
28. 8. 2017Branch prediction (implementace v procesorech)
Starší články publikované na funkcionálně.cz.
16. 11. 2017Dobrý odhad vydá za tisíc přesných počtů
15. 6. 2017Hořící křemík & násobení matic
1. 6. 2017Jazyk D a radix sort
29. 5. 2017Monoid
27. 5. 2017Von Neumannovy lži
25. 5. 2017Radix merge sort
23. 5. 2017Iterace křížem krážem
23. 5. 2017Kompaktní stringy
17. 4. 2017Mikrobenchmarky jsou těžké
19. 2. 2017Lokalita v grafech a negrafech
7. 2. 2017Vstříc řazení v lineárním čase
27. 1. 2017Maximálně negativní
24. 1. 201799.00000000000000009 problémů s floating point čísly
15. 1. 2017Závislost je špatná (pro váš program i pro váš hardware)
1. 11. 2016Od pohledu dobrý, aneb jak najít skoro stejné obrázky mezi dvěma miliony souborů za méně než deset minut
14. 10. 2016Sketches - slajdy
30. 9. 2016YOLO tree
25. 9. 2016diff a stromy
18. 9. 2016diff a komprese
9. 9. 2016Mýtus o O(1) paměti
22. 8. 2016Mergeselect
14. 8. 2016Persistentní datové struktury
26. 7. 2016Úvod do podivností moderního hardwaru, které vás budou budit ze spaní
1. 6. 2016Escape analysis
29. 5. 2016Jak rychle řadit a šetřit čas
21. 5. 2016Čím více se věci mění, tím více zůstávají stejné
29. 4. 2016How to sort out your life in O(n) time
11. 4. 2016Jak řadit v lineárním čase, křísit mrtvé a dosáhnout osvícení
26. 3. 2016Dualismus hardwaru a softwaru, strojů a virtuálních strojů
4. 3. 2016Někdy je nejchytřejší nedělat nic chytrého (další kapitola nekonečného příběhu o optimalizaci)
1. 2. 2016Kolize hashů pro mírně pokročilé
6. 1. 2016Střeva databází
12. 12. 2015I ve Scale se dá psát rychlý generický kód za použití typeclass
8. 12. 2015Jaccardovo tajemství - jak počítat podobnost množin pomalu, jak ji počítat rychle a jak při výpočtu podvádět
10. 11. 2015limit/offset stránkování nemusí být pomalé
30. 10. 2015PHP compaction hell: Kdo neamortizuje, spláče nad O(n²)
7. 10. 2015Inkluzivní cache, mnoho vláken a problémy
18. 9. 2015Jak JVM volá virtuální metody, jaká temná božstva musí vzývat, aby to bylo aspoň trochu rychlé
16. 9. 2015Grim hardware realities of functional programming
20. 7. 2015Generování kódu za běhu (ve Scale)
6. 7. 2015Pár poznámek k pár poznámkám o sloupcových databázích
25. 5. 2015L1I cache a iTLB - když ani spekulace nepomůžou
17. 5. 2015Za jak dlouho procesor vynásobí tisíc čísel
29. 4. 2015Bez typů se obejdeme, ale...
28. 3. 2015Jak optimalizovat deoptimalizací
21. 1. 2015Hyper-threading aneb "Jak sakra může běžet víc vláken na jednom jádře?"
2. 1. 2015XPath - co, proč a hlavně jak?
24. 11. 2014Branch prediction moderních procesorů
30. 10. 2014Procesory a jejich architektura (sebrané spisy)
19. 8. 2014Types will carry you over the Monads
15. 5. 2014PHP DOM, SimpleXML a Matcher
23. 4. 2014Výsledky PHP kvízu
28. 3. 2014PHP kvíz (aktualizováno)
9. 3. 2014Haldy nejsou tak velké, jak se se zdají být
7. 11. 2013Poznámka k Moorovu zákonu a rychlosti procesorů
25. 9. 2013Tak jak je to s tou rychlostí procesorů a pamětí?
6. 9. 2013Co je vůbec objektové a funckionální programování?
15. 8. 2013Všechno, co jste kdy chtěli vědět o promise, yield a generátorech v PHP, ale báli jste se zeptat
13. 8. 2013Jak vlastně psát asynchronní kód
21. 7. 2013Monády aneb Jak jsem se naučil nedělat si starosti a mít rád Haskell
11. 7. 2013JVM: Epizoda V – Paměť vrací úder
30. 6. 2013Paralelismus a asynchronnost
20. 6. 2013Kolik paměti zabírají PHP pole a objekty?
4. 6. 2013JVM a pohled objektům pod sukně
24. 5. 2013Scala - Novinky ve verzi 2.10
6. 5. 2013Rekurzivní sizeOf pro JVM
26. 4. 2013Async SQL
22. 4. 2013Anorm
13. 4. 2013Cost per element/entry in various well-known Java/Guava data structures
26. 3. 2013What every programmer should know about memory
22. 3. 2013Jak z funkcí implementovat objektový systém
20. 2. 2013Velikost objektů na JVM - Scala a specialiazce polí
30. 1. 2013Velikost objektů na JVM - Scala @specialized
25. 1. 2013SLICK
25. 1. 2013Velikost objektů v Javě - mapy
21. 1. 2013Velikost objektů v Javě
24. 12. 2012Fluent interface a funkcionální programování
16. 12. 2012Play! framework
3. 12. 2012Out of the Tar Pit
1. 1. 1970Paperlog
píše k47 (@kaja47, k47)