Showing posts with label memory usage. Show all posts
Showing posts with label memory usage. Show all posts

PHP memory consumption with Arrays and Objects (update: HHVM, phpng June-2014)

Lessons learned:
  • PHPng is significantly faster and uses significantly less memory
  • objects need more memory than arrays (+ 5-250 percent)
  • if array values are numeric, don't save them as strings!
  • saving 1M integers takes 33M of memory with PHPng (increase by factor 8)
  • saving 1M integers as strings takes 79M of memory with PHPng (increase by factor 20)
  • using SplFixedArray can reduce memory usage by 20-100 percent
  • avoid big Arrays and Objects in PHP whenever possible
    (don't use file('big_file') or explode("\n", file_get_contents('big_file')), etc.)
  • use streams whenever possible (fopen, fsockopen, etc.)
  • use generators when available with PHP 5.5 (RFC)
  • comparing 32bit to 64bit systems, memory consumption increases by 100-230 percent

If you need to save memory temporarily in your script, you can stringify your array (increasing cpu usage and runtime):

function test(){

$a = array();
for ($i=0; $i<1000000; $i++) $a[] = $i;
$a = implode(',', $a); // or $a = json_encode($a);
echo number_format(memory_get_usage(true)/1048576, 2)."\n";

}
test();
// 7.5 M / 0.64s (php), 10.4 M / 0.55s (hhvm), 7.25 M / 0.20s (phpng)

Here is a small script to show how much memory PHP needs to handle big Arrays:
the total runtime of the script is 15.5s (php), 8.4s (hhvm), 4.6s (phpng)
(tests made with PHP 5.5.9, HHVM 3.2.0-2014-06-06, PHP 5.7.0-dev-2014-06-05, 2.8GHz single-core openvz)

ini_set('memory_limit', '1024M');

function test(){

$a = '';
for ($i=0; $i<1000000; $i++) $a .= '1';
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 1.50 (php), 3.5 (hhvm), 1.50 (phpng)


$a = array();
for ($i=0; $i<1000000; $i++) $a[$i] = true;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 138.25 (php), 30.4 (hhvm), 32.75 (phpng)

$a = new stdclass;
for ($i=0; $i<1000000; $i++) $a->$i = true;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 146.25 (php), 80.92 (hhvm), 82.50 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<1000000; $i++) $a[$i] = true;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 54.25 (php), 30.56 (hhvm), 16.50 (phpng)


$a = array();
for ($i=0; $i<1000000; $i++) $a[$i] = $i;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 138.50 (php), 30.56 (hhvm), 33.25 (phpng)

$a = new stdclass;
for ($i=0; $i<1000000; $i++) $a->$i = $i;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 146 (php), 80.97 (hhvm), 82.50 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<1000000; $i++) $a[$i] = $i;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 54.25 (php), 30.58 (hhvm), 16.75 (phpng)


$a = array();
for ($i=0; $i<1000000; $i++) $a[$i] = (string)$i;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 230 (php), 61.10 (hhvm), 78.50 (phpng)

$a = new stdclass();
for ($i=0; $i<1000000; $i++) $a->$i = (string)$i;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 237.75 (php), 111.50 (hhvm), 128.25 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<1000000; $i++) $a[$i] = (string)$i;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 145.5 (php), 61.11 (hhvm), 61.75 (phpng)


$a = array();
for ($i=0; $i<1000000; $i++) $a[(string)$i] = (string)$i;
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 230 (php), 61.12 (hhvm), 78.50 (phpng)


$a = array();
for ($i=0; $i<1000000; $i++) $a[$i] = "";
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 138.5 (php), 30.62 (hhvm), 33.75 (phpng)

$a = new stdclass;
for ($i=0; $i<1000000; $i++) $a->$i = "";
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 146 (php), 81.02 (hhvm), 82.50 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<1000000; $i++) $a[$i] = "";
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 54 (php), 30.63 (hhvm), 17.00 (phpng)


$a = array();
for ($i=0; $i<1000000; $i++) $a[$i] = "hello";
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 138.5 (php), 30.64 (hhvm), 33.75 (phpng)

$a = new stdclass;
for ($i=0; $i<1000000; $i++) $a->$i = "hello";
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 146 (php), 81.04 (hhvm), 82.50 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<1000000; $i++) $a[$i] = "hello";
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 54 (php), 30.65 (hhvm), 17.25 (phpng)


$a = array();
for ($i=0; $i<1000000; $i++) $a[$i] = array();
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 222.5 (php), 30.65 (hhvm), 34.00 (phpng)

$a = new stdclass;
for ($i=0; $i<1000000; $i++) $a->$i = array();
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 230 (php), 81.05 (hhvm), 82.50 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<1000000; $i++) $a[$i] = array();
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 138 (php), 30.66 (hhvm), 17.50 (phpng)


$a = array();
for ($i=0; $i<500000; $i++) $a[$i] = array("");
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 214.75 (php), 18.67 (hhvm), 18.25 (phpng)

$a = new stdclass;
for ($i=0; $i<500000; $i++) $a->$i = array("");
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 218.25 (php), 41.81 (hhvm), 41.75 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<500000; $i++) $a[$i] = array("");
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 176.00 (php), 30.68 (hhvm), 17.50 (phpng)


$a = array();
for ($i=0; $i<500000; $i++) $a[$i] = array("hello");
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 214.50 (php), 18.68 (hhvm), 18.25 (phpng)

$a = new stdclass;
for ($i=0; $i<500000; $i++) $a->$i = array("hello");
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 218.25 (php), 41.82 (hhvm), 41.75 (phpng)

$a = new SplFixedArray(1000000);
for ($i=0; $i<500000; $i++) $a[$i] = array("hello");
echo number_format(memory_get_usage(true)/1048576, 2)."\n";
// 176 (php), 30.69 (hhvm), 17.50 (phpng)

}
test();

More details and technical explanations about PHP's memory usage can be found on nikic's blog.
More details about PHPng can be found here.

Shortly: a string or an integer in PHP are not mapped to a char-array or an integer in C. A more complex structure is used to get from dynamic typing in PHP to static typing in C:


struct _zval_struct {
zvalue_value value; // value object
zend_uint refcount__gc; // number of references to variable
zend_uchar type; // type of variable
zend_uchar is_ref__gc; // reference? (&$var)
};
typedef union _zvalue_value {
long lval; // integer and boolean
double dval; // float (double)
struct {
char *val; // string (with zero bytes)
int len; // length of the string
} str;
HashTable *ht; // array (hash table)
zend_object_value obj; // object
} zvalue_value;

Runtime vs. memory usage

Oftentimes, better runtime can result in higher memory usage. Here is an example to create some strings to test bulk inserts on Redis:

$cmd = "";
$start = microtime(true);
for ($i=0; $i<1000000; $i+=2) $cmd .= "SET entity:".$i.":key value_".($i+1)."\r\n";
echo number_format(microtime(true)-$start, 1)."s\n"; // 0.7s
echo number_format(memory_get_usage(true)/1048576, 1)." MB\n"; // 17.5 MB
echo number_format(memory_get_peak_usage(true)/1048576, 1)." MB\n"; // 17.5 MB

$cmd = "";
$start = microtime(true);
$cmd = vsprintf(str_repeat("SET entity:%d:key value_%d\r\n", 500000), range(0,1000000));
echo number_format(microtime(true)-$start, 1)."s\n"; // 0.4s
echo number_format(memory_get_usage(true)/1048576, 1)." MB\n"; // 30.8 MB
echo number_format(memory_get_peak_usage(true)/1048576, 1)." MB\n"; // 128.5 MB
(PHP 5.4.5, 2.5 GHz, win64)

We see that the same result can be calculated 3 times faster, but memory usage is 75 percent higher. Using arrays in the second version increases peak memory usage by a factor of 7. Note that on a 64bit Linux, peak memory usage is 251.3 MB in the second example.

How to write a really small and fast controller with PHP (update: benchmark Slim, Silex, Zend Framework, Symfony2)

To handle a lot of traffic, we need a fast controller with very little memory overhead. First, we implement a dynamic controller.

The design is based on the micro frameworks Slim and Silex. The first example maps the URL "http://server/index.php/blog/2012/03/02" to a function with the parameters $year, $month and $day:

// index.php, handle /blog/2012/03/02
$app = new App();
$app->get('/blog/:year/:month/:day', function($year, $month, $day) {
printf('%d-%02d-%02d', $year, $month, $day);
});
Our controller is a class named App and uses the get() function to map a GET request. Parameters mapped to the function are marked with a colon. Optional parameters are written inside brackets. Here is an example:

// handle /blog, /blog/2012, /blog/2012/03 and /blog/2012/03/02
$app = new App();
$app->get('/blog(/:year(/:month(/:day)))', function($year=2012, $month=1,
$day=1) {
printf('%d-%02d-%02d', $year, $month, $day);
});

Instead of printf(), we can use quote() to replace < and > with HTML entities. In the second example we map "index.php/product/42/super-coding-book" to a function with the parameter $id. We use * in the URL to match any character different from "/":

// index.php, handle /product/42/seo-text
$app = new App();
$app->get('/product/:id/*', function($id) use ($app) {
echo 'You selected '.$app->quote($id);
});

In the third example we map "index.php/profile/jdoe" to a function with the parameter $username and render the output with a PHP template:

// index.php, handle /profile/jdoe
$app = new App();
$app->get('/profile/:username', function($username) use ($app) {
$app->message = 'Hello '.$username;
$app->display('profile.php');
});

// profile.php
<html><body>
Message: <?= $this->quote($this->message) ?>
</body></html>

Instead of using anonymous functions, we can also forward the request to a normal function. In the next example, the request is forwarded to the static method greet() in the class Hello:

// forwards /hello/world to Hello::greet('world')
$app = new App();
$app->get('/hello/:name', 'Hello::greet');

class Hello {
static function greet($name) {
echo 'Hello '.$name;
}
}

The controller is also able to forward the request to more than one function. In this example, the request also calls header() and footer() from the Page class:

// forwards /welcome to Page::header(); User::show(); Page::footer();
$app->get('/welcome', ['Page::header', 'User::show()', 'Page::footer']);

To make testing easier, we can use a decorator subclassing to convert the output of a function to JSON:

$app = new AppJson();
$app->get('/json/range', function() {
return range(0, 10);
});

// output
[0,1,2,3,4,5,6,7,8,9,10]

Instead of named parameters, we can also use anonymous parameters with the get_p() function:

// index.php, handle /blog/2012/03/02
$app = new App();
$app->get_p('/blog/:p/:p/:p', function($year, $month, $day) {
printf('%d-%02d-%02d', $year, $month, $day);
});
Note that get_p() is 40 percent faster than get().

And finally, here is the controller:

set_exception_handler('App::exception'); // bootstrap

class App {
protected $_server = [];

public function __construct() {
// skipped mocking here
$this->_server = &$_SERVER;
}

public function get($pattern, $callback) {
$this->_route('GET', $pattern, $callback);
}

public function get_p($pattern, $callback) {
$this->_route_p('GET', $pattern, $callback);
}

public function delete($pattern, $callback) {
$this->_route('DELETE', $pattern, $callback);
}

protected function _route($method, $pattern, $callback) {
if ($this->_server['REQUEST_METHOD']!=$method) return;

// convert URL parameter (e.g. ":id", "*") to regular expression
$regex = preg_replace('#:([\w]+)#', '(?<\\1>[^/]+)',
str_replace(['*', ')'], ['[^/]+', ')?'], $pattern));
if (substr($pattern,-1)==='/') $regex .= '?';

// extract parameter values from URL if route matches the current request
if (!preg_match('#^'.$regex.'$#', $this->_server['PATH_INFO'], $values)) {
return;
}
// extract parameter names from URL
preg_match_all('#:([\w]+)#', $pattern, $params, PREG_PATTERN_ORDER);
$args = [];
foreach ($params[1] as $param) {
if (isset($values[$param])) $args[] = urldecode($values[$param]);
}
$this->_exec($callback, $args);
}

protected function _route_p($method, $pattern, $callback) {
if ($this->_server['REQUEST_METHOD']!=$method) return;

// convert URL parameters (":p", "*") to regular expression
$regex = str_replace(['*','(',')',':p'], ['[^/]+','(?:',')?','([^/]+)'],
$pattern);
if (substr($pattern,-1)==='/') $regex .= '?';

// extract parameter values from URL if route matches the current request
if (!preg_match('#^'.$regex.'$#', $this->_server['PATH_INFO'], $values)) {
return;
}
// decode URL parameters
array_shift($values);
foreach ($values as $key=>$value) $values[$key] = urldecode($value);
$this->_exec($callback, $values);
}

protected function _exec(&$callback, &$args) {
foreach ((array)$callback as $cb) call_user_func_array($cb, $args);
throw new Halt(); // Exception instead of exit;
}

// Stop execution on exception and log as E_USER_WARNING
public static function exception($e) {
if ($e instanceof Halt) return;
trigger_error($e->getMessage()."\n".$e->getTraceAsString(), E_USER_WARNING);
$app = new App();
$app->display('exception.php', 500);
}

public function quote($str) {
return htmlspecialchars($str, ENT_QUOTES);
}

public function render($template) {
ob_start();
include($template);
return ob_get_clean();
}

public function display($template, $status=null) {
if ($status) header('HTTP/1.1 '.$status);
include($template);
}

public function __get($name) {
if (isset($_REQUEST[$name])) return $_REQUEST[$name];
return '';
}
}

class AppJson extends App {
protected function _exec(&$callback, &$args) {
header('Content-Type: application/json; charset=utf-8');
echo json_encode(call_user_func_array($callback, $args));
throw new Halt(); // Exception instead of exit;
}
}

// use Halt-Exception instead of exit;
class Halt extends Exception {}

The controller fits perfectly into a high traffic scenario:
  • less than 100 lines of code
  • memory overhead less than 256 KB
  • runtime less than 1 ms

Here are some benchmarks (1.4 GHz):
Name without APC [seconds] with APC [seconds]
App 0.0009 0.0005
Slim 1.6.4 0.0159 0.0083
Silex 0.0596 0.0221
ZendFramework 1.11 0.1625 0.0631
Symfony 2.0.16 0.1968 0.0362
The numbers show that our controller is 17 times faster than Slim, 44 times faster than Silex, 72 times faster than Symfony and 126 times faster than ZF.

Here is the code:

// App
$start = microtime(true);
require 'App.php';
try {
$app = new App();
$app->get('/hello/:name', function ($name) use ($app) {
$app->name = $name;
$app->display('foo.php');
// foo.php: echo 'Hello '.$this->quote($this->name);
});
} catch (Exception $e) {}
echo ' '.(microtime(true)-$start);

// Slim
$start = microtime(true);
require 'Slim/Slim.php';
$app = new Slim();
$app->get('/hello/:name', function ($name) use ($app) {
$app->render('foo.php', ['name' => $name]);
// foo.php: echo 'Hello '.htmlspecialchars($name);
});
$app->run();
echo ' '.(microtime(true)-$start);

// Silex
$start = microtime(true);
require 'silex/vendor/autoload.php';
$app = new Silex\Application();
$app->get('/hello/{name}', function($name) use($app) {
require('templates/foo.php');
// foo.php: echo 'Hello '.htmlspecialchars($name);
});
$app->run();
echo ' '.(microtime(true)-$start);

// ZendFramework
// zf create project helloworld
// helloworld/application/controllers/IndexController.php
class IndexController extends Zend_Controller_Action {
public function helloAction() {
$this->view->name = $this->getRequest()->getParam('name');
}
}
// helloworld/application/views/scripts/index/hello.phtml
Hello <?= $this->escape($this->name); ?>
// helloworld/public.php
$start = microtime(true);
...
echo ' '.(microtime(true)-$start);
// GET /index.php/index/hello/?name=world

// Symfony2
$start = microtime(true);
require_once __DIR__.'/../app/bootstrap.php.cache';
require_once __DIR__.'/../app/AppKernel.php';
$kernel = new AppKernel('dev', false);
$kernel->loadClassCache();
$kernel->handle(Symfony\Component\HttpFoundation\Request::createFromGlobals())->send();
echo ' '.(microtime(true)-$start);
// src\Acme\DemoBundle\Controller\DemoController.php
namespace Acme\DemoBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
class DemoController extends Controller {
/**
* @Route("/hello/{name}", name="_demo_hello")
*/
public function helloAction($name) {
return new Response('Hello '.htmlspecialchars($name, ENT_QUOTES));
}
}
// GET /web/index.php/demo/hello/World

Lessons learned:
  • A good controller can speed up requests by a factor of 100
  • A good controller is the base for all kinds of performance optimizations
  • Controllers included in PHP frameworks are slow

Next: even more performance with static controller, parameter validation, Post and Put methods, File uploads, benchmark ZendFramework 2.0, Symfony 2.1

How to implement i18n without performance overhead

i18n is always difficult to implement and costs a lot performance. Normally, implementations use gettext() or a custom t()-function to translate a string. t() searches a INI or XML file for a translation key and returns the value. For example t('setting', 'de'), gives the German translation 'Einstellung'.

Typical optimizations use associative arrays (hashmaps) loaded into APC or Memcached. This requires a lot of memory for the array and produces a lot of cpu cycles for calling t() all the time. So the question is, can we do this better?

Yes! We use a just-in-time compiler for our PHP files and write the compiled PHP files to disk, so APC can cache them like regular PHP files.

An example PHP class looks like this:

// example.php
<?php

class example {
public function now() {
return '{t}Hello World, now it is:{/t} '.date('{t}m/d/Y g:i a{/t}');
}
}
The "{t}" and "{/t}" patterns serve as opening and closing tags indicating strings to be translated.

The translation files look like this:

// lang/en.ini (empty)

// lang/de.ini
Hello World, now it is: = Hallo Welt, jetzt ist es:
m/d/Y g:i a = d/m/Y H:i
Each language has one translation file (lang/<country-code>.ini). Each translation item is written into one line. The first element in the line is the English string, followed by " = " and the localized string.

Now we need a proxy to translate the PHP class before including it:

// instead of
require("core/example.php");
echo (new example())->now();

// we write
define('LANG', 'en');
require(translate('core/example.php'));
echo (new example())->now();

// input: example.php
// output: cache/<lang>_example.php_<timestamp>.php
function translate($file) {
$cache_file = 'cache/'.LANG.'_'.basename($file).'_'.filemtime($file).'.php';
// (re)build translation?
if (!file_exists($cache_file)) {
$lang_file = 'lang/'.LANG.'.ini';
$lang_file_php = 'cache/'.LANG.'_'.filemtime($lang_file).'.php';

// convert .ini file into .php file
if (!file_exists($lang_file_php)) {
file_put_contents($lang_file_php, '<?php $strings='.
var_export(parse_ini_file($lang_file), true).';', LOCK_EX);
}
// translate .php into localized .php file
$tr = function($match) use (&$lang_file_php) {
static $strings = null;
if ($strings===null) require($lang_file_php);
return isset($strings[ $match[1] ]) ? $strings[ $match[1] ] : $match[1];
};
// replace all {t}abc{/t} by tr()
file_put_contents($cache_file, preg_replace_callback(
'!\{t\}([^\{]+)\{/t\}!', $tr, file_get_contents($file)), LOCK_EX);
}
return $cache_file;
}
Before including example.php, we check if a translated version is available or build a new one. The same happens if example.php is being changed. The build takes the translation file (.ini) and converts it to a (.php) file. Then example.php gets translated with the translation file. The output is stored in cache/.

To make things even faster, we can skip file_exists() and filemtime() by using a small static compiler instead of the just-in-time compiler:

// compiler.php
static $langs = array('en', 'de');
static $files = array('core/example.php');

foreach ($langs as $lang) {
// load translations
$strings = parse_ini_file('lang/'.$lang.'.ini');

foreach ($files as $file) {
// translate .php into localized .php file
$tr = function($match) use (&$lang, &$strings) {
return isset($strings[ $match[1] ]) ? $strings[ $match[1] ] : $match[1];
};
// replace all {t}abc{/t} by tr()
file_put_contents('cache/'.$lang.'_'.basename($file), preg_replace_callback(
'!\{t\}([^\{]+)\{/t\}!', $tr, file_get_contents($file)), LOCK_EX);
}
}

// index.php
define('LANG', 'en');
require('cache/'.LANG.'_example.php');
echo (new example())->now();

Sometimes, strings need to be translated and combined with other values on different positions, depending on the language. e.g. "10 EUR" and "USD 10". This can be also done easily by using sprintf():

// PHP
$str = sprintf('{t}USD %d{/t}', 10);

// lang/de.ini
USD %d = %d EUR

By using a compiler for translations, we can make i18n a lot easier and faster!

Things you should not do in PHP (update: references)

Here is a list of things you should not do in PHP. Most of the stuff is pretty obvious, but over the years I've seen a lot of them. In most cases, these problems remain hidden until data grows above 10000 entries. So on a development system, things are always fast and there are no problems with memory limits :-)

Suppose we have a table with 100k entries:

$db->query('create table stats (c1 int(11) primary key, c2 varchar(255))');
$db->query('begin');
for ($i=0; $i<100000; $i++) {
$db->query('insert into stats values ('.$i.','.($i*2).')');
}
$db->query('commit');
Populate a big array instead of streaming results:

$result = $db->query('select * from stats');
$array = $result->fetch_all(); // 35M
// or
while ($row = $result->fetch_assoc()) $array[] = $row; // 35M
// or
while ($row = $result->fetch_array()) $array[] = $row; // 44.5M
// process $array ...

// instead of:
while ($row = $result->fetch_assoc()) { // 0.5M
// process $row
}
Sum with PHP instead of SQL:

$sum = 0;
foreach ($array as $val) $sum += $val[0]; // 44M, 1.2s

// instead of:
list($sum,) = $db->query('select sum(t1) from stats')->fetch_row(); // 0.2M, 0.1s
Sort with PHP instead of SQL:

usort($array, function ($a, $b) { return $a[0] > $b[0]; }); // 4.1s
// or
foreach ($array as $key=>$val) $helper[$key] = $val[0];
asort($helper); // 2.2s

// instead of:
$result = $db->query('select * from stats order by c1');
while ($row = $result->fetch_assoc()) { // 1.2s
Let's add a second table:

$db->query('create table stats2 (c1 int(11) primary key, c2 varchar(255))');
$db->query('begin');
for ($i=50000; $i<51000; $i++) {
$db->query('insert into stats2 values ('.$i.','.($i*2).')');
}
$db->query('commit');
Join with PHP instead of SQL (join result contains 1000 entries):

$array = $db->query("select * from stats")->fetch_all();
$array2 = $db->query("select * from stats2")->fetch_all();

foreach ($array as $key=>$val) {
foreach ($array2 as $key2=>$val2) { // 35.7M, 69s
if ($val[0] == $val2[0]) // do sth.
}
}

// instead of:
$result = $db->query('select * from stats a, stats2 b where a.t1=b.t1');
while ($row = $result->fetch_array()) { // 0.5M, 0.015s
Modify arrays without references:

$array = array();
for ($i=0; $i<1000000; $i++) $array[] = $i*2;

$start = microtime(true);
foreach ($array as &$val) $val++;
echo (memory_get_peak_usage(true)/1048576)."\n"; // 80M (32bit), 200M (64bit)
echo (microtime(true)-$start)."\n"; // 0.14s

$start = microtime(true);
foreach ($array as $key=>$val) $array[$key]++;
echo (memory_get_peak_usage(true)/1048576)."\n"; // 161M (32bit), 399M (64bit)
echo (microtime(true)-$start)."\n"; // 0.64s
more examples coming ...

Scripts running on a 1.4 GHz machine with PHP 5.4.0.