4 Apr/12

PHP memory allocation, or how to use unset(), or why to use for() instead of foreach()

Sometimes you’ll need to clean up the memory used by PHP. Especially if you have long running CLI scripts this will make sure to avoid the “PHP Fatal error: Allowed memory size of N bytes exhausted”. The basic idea is to remove unused variables from memory by using PHP’s unset() command.

In it’s simpliest form, unset() will look like this:

<?php
php $i = "is now set";
echo $i;
unset($i); // remove $i from memory, i.e. tell the garbage collector that it may unallocate $i
?>

Internally, PHP works more or less like Java when it comes to unsetting variables. Thus, unset() will not immediately unset the corresponding variable. Instead, it simply tells it’s garbage collector that the variable is no longer needed and may now be removed from memory. Good news are that the garbage collector now works quite smart and usually you don’t have to take care of how it works. Bad news are, that there are still some cases when the garbage collector fails.

I’ve just stumbled uppon the following problem:

<?php
class Foo
{
 public $id = 0;
 public $var1 = "this is a looooooong string";
 public $var2 = 102980912098;
 public $var3 = "another meaningless but loooooong string";

 public function __construct($id) {
 $this->id = $id;
 }
}

// init an array of 30.000 Foo-Objects
$fooarray = array();
for($i=0;$i<30000;$i++) {
 $v = new Foo($i);
 $fooarray[] = $v;
}

$i=0;
// now, iterate over the array
foreach($fooarray as $k => $v) {
 // do something meaningful with $v
 unset($v);
 unset($fooarray[$k]);
 if ($i++ > 1000) {
   echo "Memory allocated: ".number_format( memory_get_usage())."\n";
   $i=0;
 }
}
?>

This script simply inits an array of 30.000 objects, iterates over them and (within the iteration) unallocates the parts of the array which are no longer needed. Surprisingly, the output of this script will look like this (tested with PHP 5.3.3):

Memory allocated: 15,474,664
Memory allocated: 15,426,584
Memory allocated: 15,378,488
Memory allocated: 15,330,392
Memory allocated: 15,282,296
Memory allocated: 15,234,200
Memory allocated: 15,186,104
Memory allocated: 15,138,008
Memory allocated: 15,089,912
Memory allocated: 15,041,816
Memory allocated: 14,993,720
Memory allocated: 14,945,624
Memory allocated: 14,897,528
Memory allocated: 14,849,432
Memory allocated: 14,801,336
Memory allocated: 14,753,240
Memory allocated: 14,705,144
Memory allocated: 14,657,048
Memory allocated: 14,608,952
Memory allocated: 14,560,856
Memory allocated: 14,512,760
Memory allocated: 14,464,664
Memory allocated: 14,416,568
Memory allocated: 14,368,472
Memory allocated: 14,320,376
Memory allocated: 14,272,280
Memory allocated: 14,224,184
Memory allocated: 14,176,088
Memory allocated: 14,127,992

If you replace the last part of the script with this…

for($k=0,$c=count($fooarray);$k<$c;$k++) {
   $v=$fooarray[$k];
   // do something meaningful with $v
   unset($v);
   unset($fooarray[$k]);
   if ($i++ > 1000) {
      echo "Memory allocated: ".number_format( memory_get_usage())."\n";
      $i=0;
   }
}

…and run the script again, the output will look like this:

Memory allocated: 13,535,336
Memory allocated: 13,118,520
Memory allocated: 12,701,664
Memory allocated: 12,284,832
Memory allocated: 11,867,992
Memory allocated: 11,451,160
Memory allocated: 11,034,328
Memory allocated: 10,617,496
Memory allocated: 10,200,656
Memory allocated: 9,783,824
Memory allocated: 9,366,992
Memory allocated: 8,950,152
Memory allocated: 8,533,320
Memory allocated: 8,116,488
Memory allocated: 7,699,656
Memory allocated: 7,282,824
Memory allocated: 6,865,976
Memory allocated: 6,449,144
Memory allocated: 6,032,312
Memory allocated: 5,615,480
Memory allocated: 5,198,648
Memory allocated: 4,781,816
Memory allocated: 4,364,984
Memory allocated: 3,948,152
Memory allocated: 3,531,320
Memory allocated: 3,114,488
Memory allocated: 2,697,656
Memory allocated: 2,280,824
Memory allocated: 1,863,992

Obviously, using a foreach loop will cause problems when it comes to unallocating parts of the array you iterate over within the foreach.

Note: substituting your foreach with a for loop will solve the problem of unallocating memory. However, foreach loops are significantly faster than for loops. Keep that in mind.

Posted in PHP by Christian on April 4th, 2012 at 8:30 AM.

2 comments

2 Replies

  1. Zsolt Szilagyi May 17th 2013

    Unfortunately I have to disagree with your conclusion.
    The foreach creates a copy set of the array pointer, temporally incrementing the reference count of each array element by one.

    That means that the RAM is released by the GC once the foreach terminates. Test it yourself: Add a final

    echo “FINAL Memory allocated: “.number_format( memory_get_usage()).”\n”;

    after your foreach.

  2. Thanks for your input, Zsolt. Actually, I do not see any objection between my post and your remark. The problem in my case was that I ran into an exception within the FOREACH and somehow I had to solve the problem. Thus, even if the memory will be released after the FOREACH it doesn’t help if you get an error within the foreach.


Leave a Reply


Site tools