funkcionálně.cz

Přední český blog o funkcionálním programování, kde se o funkcionálním programování nepíše
««« »»»

Kolik paměti zabírají PHP pole a objekty?

20. 6. 2013 — k47

V PHP se zdá, že s pamětí je jenom má­lo­kdy pro­blém. Pokud ne­pře­kro­čím limit, který skript může alo­ko­vat, je všechno v po­řádku. Žádné pauzy gar­bage collec­toru (GC), žádné out of memory error, které zabijí celou apli­kaci, žádné momory leaky a žádný tlak na GC, který v marné snaze uvol­nit ubý­va­jící paměť, spálí všechny cykly CPU .

To všechno jsou dů­sledky faktu, že PHP je na­vr­ženo pro jed­no­du­chý request/re­sponse cyklus, kdy skript alo­kuje paměť a po skon­čení skriptu se celá na­jed­nou uvolní. V mnoha pří­pa­dech PHP ani GC ne­po­tře­buje.

Tento model ovliv­nil na­prosto všechno v návrhu PHP (když mluvím o PHP mám na mysli vý­chozí im­ple­men­taci od Zendu): parser musí být rychlý, pro­tože se skripty par­sují pořád dokola, skript se ne­pře­kládá do na­tiv­ního kódu (JIT), ale vždycky se in­ter­pre­tuje, GC je velice jed­no­du­chý re­f­count s při­da­nou de­tekcí cyklů a ne­pro­vádí kom­pakci.

To všechno fun­guje vý­borně, když PHP po­u­ží­vám tak jak to bůh za­mýš­lel, ale velice rychle začnu na­rá­žet na různé pro­blémy u dlouho bě­ží­cích skriptů a fra­meworků jako na­pří­klad React.PHP: skript může běžet celé týdny nebo měsíce a v ta­ko­vém pří­padě se vy­platí pře­lo­žit horké smyčky do na­tiv­ního kódu, pro­tože GC ne­pro­vádí kom­pakci, určitý pro­gram může frag­men­to­vat paměť, která nikdy nebude uvol­něna a nebude ji možné efek­tivně využít (jako se mi po­vedlo když jsem tes­to­val tuto zrůd­nost)

Ale hlavně člověk začne po­zo­ro­vat, jak straš­livé množ­ství paměti PHP spo­tře­buje a že prak­ticky není možné se z této pa­mě­ťové smyčky vy­vlék­nout a vy­tvo­řit efek­tiv­nější datové struk­tury (ono je to tech­nicky možné, ale za jakou cenu?). O Javě se říká, že je pa­mě­ťově ne­na­žraná, ale přesto nesahá ani po kolena PHP ve vel­ko­lepé ne­e­fek­ti­vitě.

To je způ­so­beno dvěma aspekty jazyka/run­time:

  1. všechny hod­noty jsou bo­xo­vané ve struk­tu­rách na­zva­ných zval (+ každé čtení hod­noty zna­mená skok na poin­ter, což s sebou může nést hodně cache miss).
  2. pole a dy­na­mické ob­jekty jsou im­ple­men­to­vané jako řazené ha­shmapy

O zva­lech a polích si můžete pře­číst ve vý­bor­ném článku How big are PHP arrays (and values) really?. Z něho vy­plývá, že pole jsou ztra­cený případ. Je to z části způ­so­beno tím, že toho umějí příliš mnoho – pole jsou ve sku­teč­nosti ha­shmapy zvalů, které si ucho­vá­vají pořadí na před­chozí a ná­sle­du­jící prvek. Jenom má­lo­kdy po­tře­buji takové cho­vání, které je často ma­toucí, pro­tože není zřejmé, jak přesně se má v daných pří­pa­dech tato slo­žitá datová struk­tura po­u­ží­vat. Vět­ši­nou po­tře­buji buď pole, nebo mno­žinu nebo mapu, ale jenom má­lo­kdy po­tře­buji všechno na­jed­nou.

Udělal jsem proto ně­ko­lik testů, které zjiš­ťují jak efek­tivně na­klá­dají s pamětí různé datové struk­tury v PHP (pole, dy­na­mický objekt, třída, Spl­Fi­xe­dArray) za růz­ných okol­ností a v růz­ných ver­zích PHP (5.3.10, 5.4.9 a 5.5-RC2). Každý test vždy vy­tvo­řil da­to­vou struk­turu, která ob­sa­huje 100000 ele­mentů. Vý­sledky jsou za­jí­mavé a uka­zují za jakých okol­ností se vyhnou jakým struk­tu­rám, když chci do paměti narvat co nejvíc dat. Je z nich také patrný mírný pokrok mezi PHP 5.3 a 5.4.

Tes­to­vací skript můžete najít tady.


Vý­sledky:

x PHP 5.3.10 PHP 5.4.9 PHP 5.5-RC2
x total elem total elem total elem
array_fill nulls 14155776 141.56 10485760 104.86 10485760 104.86
array_fill con­stant 14155776 141.56 10485760 104.86 10485760 104.86
array_fill value 14155776 141.56 10485760 104.86 10485760 104.86
range 22020096 220.20 15204352 152.04 15204352 152.04
Spl­Fi­xe­dArray empty 1835008 18.35 1572864 15.73 1572864 15.73
Spl­Fi­xe­dArray nulls 9961472 99.61 6291456 62.91 6291456 62.91
Spl­Fi­xe­dArray const 9961472 99.61 6291456 62.91 6291456 62.91
pri­vate fields 101974016 1019.74 32768000 327.68 32768000 327.68
public fields de­fault 99614720 996.15 32768000 327.68 32768000 327.68
public fields set 123469824 1234.70 47185920 471.86 47185920 471.86
public fields set one 107479040 1074.79 37486592 374.87 37486592 374.87
public fields add one 120324096 1203.24 89391104 893.91 89391104 893.91
arrays con­stant keys 107741184 1077.41 72876032 728.76 72876032 728.76
arrays va­ri­a­ble keys 107741184 1077.41 75235328 752.35 75235328 752.35

Jak je vidět, PHP 5.4 a 5.5 jsou na tom na­prosto iden­ticky, ale oproti 5.3 vy­ka­zují značné zlep­šení.

Z prv­ních třech řádků je vidět, že pokud vy­tvo­řím pole přes array_fill s hod­no­tou null, kon­stan­tou nebo $pro­měn­nou, za­bí­rají stejné množ­ství paměti. Když vy­tvo­řím stejné pole přes range, zabere při­bližně o 50% více místa. Jde o to, že se hod­nota ne­vy­tváří pořád dokola, ale sdílí se ten samý zval (re­f­count). Když se pak nějaká takto za­lin­ko­vaná hod­nota změní, vy­tvoří se sa­mo­statná kopie.

Za­jí­mavé cho­vání uka­zuje Spl­Fi­xe­dArray, které není in­terně re­pre­zen­to­váno jako hash ta­bulka, ale jako ploché pole poin­terů na zvaly (jde o dy­na­mické pole). Když Spl­Fi­xe­dArray vy­tvo­řím aniž bych do něj něco při­řa­dil, zabírá velice málo místa. Dá se před­po­klá­dat, že je plné null poin­terů. Ale když do něj cokoli při­řa­dím, ob­sa­huje poin­tery na zvaly, včetně pří­padu kdy při­řa­zuji null. Jak je vidět Spl­Fi­xe­dArray oproti poli v tomto kon­krét­ním pří­padě po­tře­buje jenom cca 42% paměti.

Další testy pro­vě­řují ob­jekty a třídy. In­stance tříd s pri­vát­ními i sou­kro­mými atri­buty za­bí­rají stejné množ­ství paměti. Ale když tyto atri­buty pře­píšu, spo­třeba paměti se zvětší a vypadá to, že roste úměrně s počtem pře­psa­ných atri­butů. Za po­zor­nost stojí, že objekt se třemi in­to­vými atri­buty spo­tře­buje přes 300 bajtů paměti včetně ná­kladů pole ve kterém jsou ulo­ženy (v Javě by to bylo 48 bajtů).

Ná­sle­dují dy­na­mické struk­tury: pole a ob­jekty bez de­kla­ro­va­ných atri­butů. Jak je vidět, pole po­tře­bují více paměti než in­stance tříd, při­bližně dva­krát. Plně dy­na­mické ob­jekty nebo in­stance tříd, kterým jsem při­řa­dil dy­na­mický atri­but pak po­tře­bují ještě o něco víc. Důvod je popsán tady. Jde o to, že jména de­kla­ro­va­ných atri­butů jsou ulo­ženy v de­fi­nici třídy, ale jména všech dy­na­mic­kých atri­butů/klíčů musejí být ulo­ženy v každé in­stanci ob­jektu nebo pole jako klíče hash ta­bulky (a po­cho­pi­telně delší jména za­be­rou víc paměti). A jak vidět, tak stačí, když do in­stance třídy za­pí­šeme jeden dy­na­mický atri­but a struk­tura ob­jektu se au­to­ma­ticky de­op­ti­ma­li­zuje na hash-ta­bulku.

Co z toho tedy vy­plývá: 1) když po­tře­bu­jete li­ne­ární struk­turu, po­u­ží­vejte Spl­Fi­xe­dArray i když bude z větší částí plná null hodnot 2) ob­jekty, které de­kla­rují vlastní pro­měnné za­bí­rají nejméně místa, pak pole a na­ko­nec plně dy­na­mické ob­jekty 3) ne­při­řa­zujte dy­na­mické atri­buty do in­stancí tříd, to víc jak zdvoj­ná­sobí jejich spo­třebu paměti

@kaja47, kaja47@k47.cz, deadbeef.k47.cz, starší články