[PHP] Geheugen verdwijnt in loops (limietprobleem)

Pagina: 1
Acties:

Onderwerpen


Acties:
  • 0 Henk 'm!

  • RwD
  • Registratie: Oktober 2000
  • Niet online

RwD

kloonikoon

Topicstarter
Ik maak in een programma veel gebruik van for en foreach constructies. Nu is er een samenloop van omstandigheden waardoor ik heel veel gegevens op moet halen en op de server van een klant tegen de geheugenlimiet aan loop. Dus er van uitgaande dat ik inefficient heb geprogrammeerd, of minstens kan verbeteren, heb ik uitgezocht waar ik geheugen verlies. Dit blijkt voornamelijk in de for/foreach lussen te zijn. Om te testen waar het fout gaat heb ik de volgende code gemaakt:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
echo "1) start:\t" . memory_get_usage() . "\n";

$items = array();
for($i = 0; $i < 999; ++$i) {
        $items[$i]['val1'] =  str_repeat('*', 9999);
        $items[$i]['val2'] =  str_repeat('*', 9999);
        $items[$i]['val3'] =  str_repeat('*', 9999);
}

echo "2) items:\t" . memory_get_usage() . "\n";

unset($i, $items);

echo "3) unset:\t" . memory_get_usage() . "\n";
?>
De output is:
code:
1
2
3
1) start:   62968
2) items:   30437256
3) unset:   127624


Nu gaat het hier nog niet om schokkende hoeveelheden. Maar zodra ik wat meer doe binnen de lussen en dit een aantal keer tegen kom in verschillende functies, dan blijft er meer geheugen over.

Mijn vraag is eigenlijk of ik iets verkeerds doe, of juist iets niet. Dit verlies is permanent gedurende de hele uitvoering, het komt ook voor als ik een functie aan roep die daarna resultaat terug geeft.

Ik heb uitgezocht hoe php met het geheugen om gaat, en geheugen dat niet meer gebruikt is (refcount=0) zou automatisch vrij gegeven worden. Aan het einde van mijn voorbeeldscript is er geen variabele meer over die gebruikt kan worden, dus het geheugenverbruik zou moeten zijn wat die was aan het begin van het script;

bovendien; als ik $real_usage (eerste parameter memory_get_usage) op true zet is het verschil groter:
code:
1
2
3
1) start:   262144
2) items:   30670848
3) unset:   6815744
Maar ik denk niet dat ik me over deze laatste cijfers druk hoef te maken...

Weet iemand meer van dit onderwerp?

Acties:
  • 0 Henk 'm!

  • hostname
  • Registratie: April 2009
  • Laatst online: 16-09 09:13
Dat het geheugengebruik in de loop omhoog gaat is logisch, want je vult daar 3 variablen met een een string van 9999 bytes (999 iteraties x 3 strings x 9999 bytes = 29MB). Maar daar was je zelf ook al achter.

Verder blijft het geheugengebruik nooit helemaal gelijk. Er worden nog allerlei dingen bijgehouden. Een concreet voorbeeld dat ik me nu kan bedenken is dat de hash table (waarin alle variablen staan) vergroot wordt en na het unsetten() niet meer verkleind wordt.

Het verschil tussen start en unset is bij zowel 500, 1000 als 1500 iteraties ongeveer even groot (zelf getest). Is waarschijnlijk dus gewoon wat interne dingen die PHP bijhoud of vergroot waarvoor extra geheugen nodig is, maar verder niet iets waar ik me zorgen over zou maken.

Gebruik je PHP 5.2 of 5.3? In 5.3 is er behoorlijk wat werk verricht aan de Garbage Collector. Zie ook dit.

Acties:
  • 0 Henk 'm!

  • Voutloos
  • Registratie: Januari 2002
  • Niet online
hostname schreef op zaterdag 13 maart 2010 @ 12:50:
Dat het geheugengebruik in de loop omhoog gaat is logisch, want je vult daar 3 variablen met een een string van 9999 bytes (999 iteraties x 3 strings x 9999 bytes = 29MB). Maar daar was je zelf ook al achter.
De code in de topicstart toont ook zeker geen probleem aan.

RwD, je zal toch echt wat meer moet profilen/meten aan het echte script. Als je de bottleneck(s) gevonden hebt is dat wellicht interessantere code om hier neer te zetten en te tweaken.

{signature}


Acties:
  • 0 Henk 'm!

  • Gomez12
  • Registratie: Maart 2001
  • Laatst online: 17-10-2023
RwD schreef op zaterdag 13 maart 2010 @ 12:31:
Mijn vraag is eigenlijk of ik iets verkeerds doe, of juist iets niet. Dit verlies is permanent gedurende de hele uitvoering, het komt ook voor als ik een functie aan roep die daarna resultaat terug geeft.
In dit voorbeeld : jazeker.

Je hebt een echo ertussen staan die geheugen inneemt, je hebt een memory_get_usage ertussen staan.
Dit beinvloed het allemaal. Een echo reserveert gewoon geheugen voor de output bijv.

Gooi die for...each loop en unset eens 10.000x achter elkaar ( zonder extra dingen ertussen ) en daarna 100x achter elkaar, zie je dan een geheugen verschil tussen 10.000x en 100x dan heb je mogelijk te maken met een probleem ( moet je eerst wel weer uitvinden of php het intern niet wegoptimaliseert als je 10.000x hetzelfde doet, maar dat dacht ik niet ).

Simpel gezegd gebruikt elke functie geheugen en het is afhankelijk van veel factoren of dat gelijk weer vrijgegeven wordt ( outputbuffering / zlib_compression bijv )

Oftewel maak een goede testcase ipv dit...

En btw, er zitten wel wat geheugenproblemen in php, de garbage collector is niet 100%. Maar dit is echt iets in de orde van grootte dat als jij een continu draaiend proces hebt wat continu bezig is dat je dan na een half jaar oid geheugenproblemen kan verwachten. Wat je nu ziet is volgens mij enkel maar wat interne cachings van php
Ik heb uitgezocht hoe php met het geheugen om gaat, en geheugen dat niet meer gebruikt is (refcount=0) zou automatisch vrij gegeven worden. Aan het einde van mijn voorbeeldscript is er geen variabele meer over die gebruikt kan worden, dus het geheugenverbruik zou moeten zijn wat die was aan het begin van het script;

bovendien; als ik $real_usage (eerste parameter memory_get_usage) op true zet is het verschil groter:
code:
1
2
3
1) start:   262144
2) items:   30670848
3) unset:   6815744
Maar ik denk niet dat ik me over deze laatste cijfers druk hoef te maken...

Weet iemand meer van dit onderwerp?
[/quote]

Acties:
  • 0 Henk 'm!

  • RwD
  • Registratie: Oktober 2000
  • Niet online

RwD

kloonikoon

Topicstarter
Nou, de eigenlijke code scheelt bijna niks. Ik moet 33 bestanden verwerken, 1 voor 1. Met onderstaand fragment uit de code kan ik 6 bestanden verwerken en daarna loop ik tegen de limiet aan
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
foreach ( $lines as $line ) {
    $rider = array();
    $rider['Id'] = $riderId = substr($line, 0, 6);
    $rider['NameLast'] = substr($line, 6, 24);
    $rider['NameInfix'] = substr($line, 30, 10);
    $rider['NameFirst'] = substr($line, 50, 30);
    $rider['Gender'] = (substr($line, 80, 1) == 'M' ? 'M' : 'F');
    $rider['DateOfBirth'] =  substr($line, 81, 10);

    $ridersArray[$rider['Id']] = $rider;

    unset($rider);
}
Als ik het geheugen bij houdt zie ik het bij ieder bestand met een flink aantal bytes stijgen.

Die code heb ik uiteindelijk veranderd in:
PHP:
1
2
3
4
5
6
7
8
9
10
11
foreach ( $lines as $line ) {
    $riderId = substr($line, 0, 6);
    $ridersArray[$riderId]['Id'] = $riderId;
    $ridersArray[$riderId]['NameLast'] = substr($line, 6, 24);
    $ridersArray[$riderId]['NameInfix'] = substr($line, 30, 10);
    $ridersArray[$riderId]['NameFirst'] = substr($line, 50, 30);
    $ridersArray[$riderId]['Gender'] = (substr($line, 80, 1) == 'M' ? 'M' : 'F');
    $ridersArray[$riderId]['DateOfBirth'] =  substr($line, 81, 10);

    unset($riderId);
}
Met dit als enige verschil haal ik het 33ste bestand met net zo veel geheugen in gebruik als na de verwerking van bestand 2 met de voorgaande code. Ook met het weg laten van de unsets of de initiele assignments gaat het geheugen hard weg.

Deze code heb ik niet direct gepost omdat ik niet dacht dat het handig is omdat je dit niet zelf kunt testen en mijn voorbeeld ook niet al het gallocceerde geheugen weer vrij lijkt te geven. Want voor zover ik begrijp geeft de memory_get_usage(false) het gereserveerde geheugen weer voor variabelen.

Edit
Even bovenstaande twee codeblokken aangepast omdat ik een paar suggesties kreeg die ik zelf eerder al getest had en per ongeluk had laten staan

[ Voor 5% gewijzigd door RwD op 13-03-2010 15:50 ]


Acties:
  • 0 Henk 'm!

  • RwD
  • Registratie: Oktober 2000
  • Niet online

RwD

kloonikoon

Topicstarter
Gomez12 schreef op zaterdag 13 maart 2010 @ 13:25:
[...]

In dit voorbeeld : jazeker.

Je hebt een echo ertussen staan die geheugen inneemt, je hebt een memory_get_usage ertussen staan.
Dit beinvloed het allemaal. Een echo reserveert gewoon geheugen voor de output bijv.

[...]

Oftewel maak een goede testcase ipv dit...

[...]
Ja, dat heb ik getest voordat ik dit topic startte en het geheugengebruik aan het einde van het script met ALLEEN de echo's en geen for lus of unsets blijft vrijwel gelijk, komen 100 nogwat byte bij. Dus dat is zeker niet het probleem. Oftewel; je hebt dit niet getest voordat je jouw antwoord postte.

Desalniettemin wil ik best wel kijken of ik een voorbeeld voor elkaar kan krijgen dat het probleem reproduceerbaar en enigzins realistisch kan laten zien. Maar dat is niet 1-2-3 gedaan...

Ik heb zojuist geprobeerd een testcase samen te stellen, maar ik kan geen groter geheugenprobleem voor elkaar krijgen. Toch snap ik niet waarom het enige verschil tussen de twee blokken code in de werkelijke code wel werkt (de variabelen staan in een korte functie) en als ik wil dat het fout gaat niet. Vandaar dat ik hier de gehele functie er bij heb geplakt, ik verwacht niet dat het daar duidelijker van wordt:
PHP:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function processRidersFile($fileName) {
    if ( !file_exists($fileName) ) {
        return array();
    }

    $ridersArray = array();

    // Roept require aan als dit bestand nog niet eerder ingevoegd was
    // Net als require_once, maar sneller
    CoreApi::relativeRequireOnce('modules/signin/classes/Utilities.php');

    $lines = file($fileName);

    foreach ( $lines as $line ) {
        $rider = array(); 
        
        $rider['Id'] = $riderId = trim(substr($line, 0, 6), " \t.*"); 
        $rider['NameLast'] = trim(substr($line, 6, 24), " \t.*"); 
        $rider['NameInfix'] = trim(substr($line, 30, 10), " \t.*"); 
        $rider['NameFirst'] = trim(substr($line, 50, 30), " \t.*"); 
        $rider['Gender'] = (substr($line, 80, 1) == 'M' ? 'M' : 'F'); 
        
        $riderDOB = explode("-", trim(substr($line, 81, 10), " \t.*"));
        $ridersArray[$riderId]['DateOfBirth'] = Utilities::dateTimeFormat($riderDOB[2] . "-" . str_pad($riderDOB[1], 2, "0", STR_PAD_LEFT) . "-" . str_pad($riderDOB[0], 2, "0", STR_PAD_LEFT));
        
        if ( !array_key_exists($rider['Id'], $ridersArray) ) { 
            $ridersArray[$rider['Id']] = $rider; 
        } 

            unset($rider);
    }

    unset($lines);

    return $ridersArray;
}
(ik heb wat namen veranderd om de regels korter te maken. ik gebruik graag lange omschrijvende namen)

Edit
Kleine aanpassingen aan de code vanwege suggesties en een foutje van mezelf

[ Voor 51% gewijzigd door RwD op 13-03-2010 15:52 ]


Acties:
  • 0 Henk 'm!

  • hostname
  • Registratie: April 2009
  • Laatst online: 16-09 09:13
Op welke regel krijg je je memory error? Op regel 14 laad je namelijk de hele file in het geheugen, en als die file groot is kan je het inderdaad gebueren dat je daar tegen de memory-limiet aanloopt.
Bovendien is het logisch dat het geheugengebruik met elke file stijgt, want je leest immers de file in en die data plaats je in het geheugen (of je doet een unset() van het resultaat tussen elke aanroep van de functie). Het verschil tussen die 2 lappen code zou kunnen zitten in array_key_exists().
& welke php versie?

ik ben wel benieuwd hoe je require_once sneller hebt geimplementeerd...

Acties:
  • 0 Henk 'm!

  • RwD
  • Registratie: Oktober 2000
  • Niet online

RwD

kloonikoon

Topicstarter
hostname schreef op zaterdag 13 maart 2010 @ 14:30:
Op welke regel krijg je je memory error? Op regel 14 laad je namelijk de hele file in het geheugen, en als die file groot is kan je het inderdaad gebueren dat je daar tegen de memory-limiet aanloopt.
Nou ja, het probleem is niet zo zeer dat hij bij een regel te veel geheugen vraagt, maar meer dat na ieder bestand meer en meer geheugen gereserveerd blijft terwijl ik alles vrij geef
Bovendien is het logisch dat het geheugengebruik met elke file stijgt, want je leest immers de file in en die data plaats je in het geheugen (of je doet een unset() van het resultaat tussen elke aanroep van de functie).
Nou, eigenlijk was ik vergeten ridersArray uit de parameters te halen. Maar de return variabele wordt ge-unset zodra die verwerkt is buiten deze functie. En alle variabelen in de functie doe ik of expliciet unsetten (tijdelijk, alleen om het zeker te weten), of ze zouden vrij gemaakt moeten worden zodra we de functie verlaten!!
Het verschil tussen die 2 lappen code zou kunnen zitten in array_key_exists().
& welke php versie?
Nee, eigenlijk had ik daar beter op moeten letten, dat was een stukje logica die in beide wel en niet heeft gezeten, zonder verschil.
ik ben wel benieuwd hoe je require_once sneller hebt geimplementeerd...
PHP:
1
2
3
4
5
6
7
function requireOnce($file) {
    static $loaded;
    if (!isset($loaded[$file])) {
        require_once($file);
        $loaded[$file] = 1;
    }
}
Ik weet niet of het nog steeds sneller is, de code is uit 2006.
Pagina: 1