Проекты → Копипаст рулит! В PHP.

Предыстория. Заметили, что в одном из наших проектов один из модулей выполняется довольно долго. Суть модуля — получить данные из базы, очистить их и отдать клиенту. Запустили профайлер, держа в голове, что тормозит либо запрос, либо очистка данных. Результат немного удивил (цифры отражают только порядок):

Время всего модуля: 0,68 сек
Время получения данных из базы: 0,02 сек
Время очистки данных: 0,34 сек

Скорость запроса устраивает (получения порядка 5-8к строк по нескольким фильтрам). Смотрим код очистки данных, он примерно такой (схематично):

function main()
{
    $data = storage::getData($date, $driver, $params);
    return clearData($data);
}

function getData($date, $driver, array $params = [])
{
    ...
}

function clearData($data)
{
    ...
    foreach ($data['items'] as $item)
    {
        $a = func1($item);
        $b = func2($item);
        $item['res] = [$a, $b];
    }
    ...
}

Важный момент в этом коде — вызов func1 и func2. Каждый из них отрабатывает за 0,0000001 сек. В цикле из 5-8к элементов — 0,005 сек. А вот весь блок в foreach выполняется за 0,2 сек! Последняя строка в блоке не значительна (но на всякий случай и ее перепроверили, если честно). Тут начинается самое интересное…

Вспоминаем, что кто-то из нас где-то читал, что вызов функций/методов в php является дорогой операцией. Проверяем:

<?
$count = 1000000;

function t($a) {
	return $a + 1;
}

function tl1(&$a) {
	$a += 1;
}

function tl2(&$a) {
	$a++;
}

class test {
	public static function staticTest($a) {
		return $a + 1;
	}
	public function nonStaticTest($a) {
		return $a + 1;
	}
}

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	$a = $a + 1;
}
echo '$a = $a + 1: ', "\t\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	$a += 1;
}
echo '$a += 1: ', "\t\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	$a++;
}
echo '$a++: ', "\t\t\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	++$a;
}
echo '++$a: ', "\t\t\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	$a = t($a);
}
echo '$a = t($a): ', "\t\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	tl1($a);
}
echo 'tl1($a): ', "\t\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	tl2($a);
}
echo 'tl2($a): ', "\t\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$i = 0;
$a = 1;
while ($i++ < $count) {
	test::staticTest($a);
}
echo 'test::staticTest($a): ', "\t\t", (microtime(true) - $time), PHP_EOL;

$time = microtime(true);
$test = new test;
$i = 0;
$a = 1;
while ($i++ < $count) {
	$test->nonStaticTest($a);
}
echo '$test->nonStaticTest($a): ', "\t", (microtime(true) - $time), PHP_EOL;

И тут у нас был легкий шок:

$a = $a + 1 0.033540010452271
$a += 1 0.040336132049561
$a++ 0.054153919219971
++$a 0.030272960662842
$a = t($a) 0.15388703346252
tl1($a) 0.1360011100769
tl2($a) 0.12306308746338
test::staticTest($a) 0.12704300880432
$test->nonStaticTest($a) 0.14065408706665

Я выделил 2 строки. Первая — результат прямого вычисления в цикле. Если вычисления вынести в функцию — получим вторую выделенную строку и разницу в скорости в 5х! На 1М итераций — пятикратное замедление скорости! Это жесть. Кстати, это практически не зависит от версии php.

Для себя сделали вывод — выносить часто используемые участки кода в отдельные методы, в php, нельзя! Переписываем критичные к скорости части кода…

P.S.: сделал чуть более сложный тест, чтобы было видно, что от самих вычислений мало что зависит. Изменилась разница во времени вызовов, но сам порядок остался близким к предыдущему тесту.