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 (před 6 lety) — 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:

xPHP 5.3.10PHP 5.4.9PHP 5.5-RC2
xtotalelemtotal
array_fill nulls14155776141.5610485760
array_fill con­stant14155776141.5610485760
array_fill value14155776141.5610485760
range22020096220.2015204352
Spl­Fi­xe­dArray empty183500818.351572864
Spl­Fi­xe­dArray nulls996147299.616291456
Spl­Fi­xe­dArray const996147299.616291456
pri­vate fields1019740161019.7432768000
public fields de­fault99614720996.1532768000
public fields set1234698241234.7047185920
public fields set one1074790401074.7937486592
public fields add one1203240961203.2489391104
arrays con­stant keys1077411841077.4172876032
arrays va­ri­a­ble keys1077411841077.4175235328

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