A Study of PHP Arrays: Performance vs Memory

Written by AbiusX on . Posted in Development

While working on PureTextRender package, I realized some serious limits in PHP arrays. The mentioned package renders text into BMP images using pure PHP (no GD), and for that it requires a lot of arrays to be filled and traversed. The original package only supported small images due to typical PHP memory limit (128MB) though images were monochrome BMP! Before going any further, let’s see some PHP benchmark code:

1
2
3
4
5
<?php //array_fill() Memory Usage	 	 
$startMemory=(memory_get_usage()/1024);	 	 
$a=array_fill(0, 1000, 0);	 	 
echo PHP_EOL.((memory_get_usage()/1024)-$startMemory)." KB".PHP_EOL;	 	 
//Result: 94.3828125 KB

As you see, a thousand array elements take 94 Kilobytes of memory, resulting in 94 bytes per element on average. Lets do more experiments. I will omit the $startMemory and the line that echo’s the memory difference in the rest of the examples in this article, and I’m running all this on a 64bit OS X:

1
2
3
4
//PHP Native Array Memory Usage	 	 
$a=array();	 	 
for ($i=0;$i<1000;++$i)	 	 
 $a[$i]=$i;

Output: 141.375 KB

As you can see, this example takes almost 50% more memory, though it is also populating a 1000 element array. It doesn’t matter what the value is in the code, I can replace =$i with =0 and it would consume the same amount of memory. A simple explanation would be that the hash table used for storing array keys needs expansion over and over, and garbage collector doesn’t get a chance to clean up before the end of the script, keeping the previous versions in place. An expansion policy of over 50% would result in about 50% overhead in memory usage.

Now the following code will only consume 20KB in spite of an expected 988 KB:

1
2
//2D array_fill() Memory Usage	 	 
$a=array_fill(0,100,array_fill(0,100,0));

The reason for this behavior is that a single array of 100 elements is created in the internal expression, and assigned by reference to all outer 100 arrays, so the resulting memory consumption is (100+1)X instead of (100*100)X. Lets repeat all the experiments using SplFixedArray:

1
$a=new SplFixedArray(1000); //8.5703125 KB

The above line only defines the fixed array, and the below code populated it as well:

1
2
3
4
//SplFixedArray MemoryUsage	 	 
$a=new SplFixedArray(1000);	 	 
for ($i=0;$i<1000;++$i)	 	 
 $a[$i]=$i;

 

Now this sample uses 55.6015625 KB of memory, making an average of 55 bytes per element. The ideal situation would be 8 bytes per element (as happens in the definition), since 64bit systems have 8 byte int elements, but the extra space is consumed by the hash table and its indices. As you can see, a 3-dimensional array of size 100 would quickly chomp up PHP memory and leave nothing to work with. Before going further with memory footprints, lets compare performance of SplFixedArray with native arrays:

1
2
3
4
5
6
7
//SplFixedArray Performance	 	 
$t=microtime(1);	 	 
$a=new SplFixedArray(1024*1024);	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //3.4949779510498 ms	 	 
for ($i=0;$i<1024*1024;++$i)	 	 
 $a[$i]=$i;	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //228.07097434998 ms

Creation of the array is pretty fast, but population takes a good chunk of time. Lets see how much of that time is wasted in the loop:

1
2
3
4
//PHP Loop Performance	 	 
$t=microtime(1);	 	 
for ($i=0;$i<1024*1024;++$i);	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //138.4379863739 ms

Well, it’s mostly PHP’s fault then. It takes almost 100 miliseconds to fill one million elements of SplFixedArray. Lets see how native PHP arrays do:

1
2
3
4
5
6
7
8
9
//PHP Native Array Performance	 	 
$t=microtime(1);	 	 
$a=array();	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //0.003814697265625 ms	 	 
for ($i=0;$i<1024*1024;++$i);	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //141.56699180603 ms	 	 
for ($i=0;$i<1024*1024;++$i)	 	 
 $a[$i]=$i;	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //329.04195785522 ms

And when using arrray_fill:

1
2
3
4
5
6
7
8
9
//PHP array_fill() Performance	 	 
$t=microtime(1);	 	 
$a=array_fill(0,1024*1024,0);	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //78.655004501343 ms	 	 
for ($i=0;$i<1024*1024;++$i);	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //136.88087463379 ms	 	 
for ($i=0;$i<1024*1024;++$i)	 	 
 $a[$i]=$i;	 	 
echo ((microtime(1)-$t)*1000)." ms".PHP_EOL;$t=microtime(1); //232.02109336853 ms

Not bad at all! In fact array_fill is taking half the time of filling the array already. This means that assigning values to the array are much faster than expanding the hash table.
Overall, if it takes half a second to create, fill and traverse a one mega-entry array in PHP (~140MB). A typical web PHP setup has 128MB of memory limit (per instance) and 30 seconds of running time limit. They kinda match, because if you could come up with a faster storage that consumed more memory, you would be on a memory shortage, and if you came up with a denser storage, you would probably be too slow to fully utilize it. With all that being said, it appears that PHP focuses more on speed rather than memory, since most applications run more than one pass on pieces of their data.
Before going further, lets compare everything to an equivalent C code:

Welcome

I'm Abbas Naderi Afooshteh, also known as Abius and AbiusX. A software engineer and a renowned security expert, I'm currently OWASP chapter leader of Iran, owner of many OWASP projects, a member of ISSECO and CIO of Etebaran Informatics. For more details of what I do and what I can do, check my resume.