Вопросы производительности

Предыдущий раздел уже упоминал, что простой сбор корней незначительно влияет на производительность, но это если сравнивать PHP 5.2 с PHP 5.3. Хотя запись корней в буфер по сравнению с отсутствием такой записи в PHP 5.2 замедляет работу приложения, другие изменения в работе PHP 5.3 во время выполнения кода предотвратили даже проявление этой конкретной потери производительности.

Производительность страдает в двух главных областях. Первая область — уменьшение размера памяти, которую PHP тратит на запись корней, а вторая — задержка во время выполнения кода, когда механизм сборки мусора очищает память. Рассмотрим обе проблемы.

Уменьшение размера памяти

Первая причина появления в языке механизма сборки мусора, состоит в уменьшении размера памяти, которую занимает мусор, путём очистки переменных с циклическими ссылками в тот момент, когда выполнятся предварительные условия. В реализации PHP сборка мусора начинается, как только заполняется корневой буфер или при вызове функции gc_collect_cycles(). График ниже показывает, как скрипт под графиком занимает память в PHP 5.2 и PHP 5.3, без учёта памяти, которую занимает сам PHP при запуске.

Пример #1 Пример использования памяти

<?php

class Foo
{
public
$var = '3.14159265359';
public
$self;
}

$baseMemory = memory_get_usage();

for (
$i = 0; $i <= 100000; $i++) {
$a = new Foo();
$a->self = $a;
if (
$i % 500 === 0) {
echo
sprintf('%8d: ', $i), memory_get_usage() - $baseMemory, "\n";
}
}

?>
Сравнение потребления памяти в PHP 5.2 и PHP 5.3

В этом академическом примере создаётся объект, в котором устанавливается свойство, которое указывает на сам объект. Когда в скрипте переменной $a на следующей итерации цикла повторно присваивается значение, происходит типичная утечка памяти. В примере утекает память для двух zval-контейнеров — контейнера объекта и контейнера свойства объекта, — но алгоритм находит только один корень: переменную, которую удалили. Как только после 10 000 итераций (если PHP-сборка разрешает только 10 000 корней) корневой буфер заполняется, срабатывает механизм сборки мусора и память, которую занимают эти корни, освобождается. Этот процесс хорошо виден на неравномерном графике потребления памяти PHP 5.3: после каждых 10 000 итераций график проседает. Сам механизм в примере совершает не много работы, потому что структура утечек проста. Из графика видно, что максимальное потребление памяти в PHP 5.3 составило около 9 МБ, тогда как в PHP 5.2 потребление памяти продолжает расти.

Замедление работы

Вторая область, в которой механизм сборки мусора влияет на производительность, — потеря времени, которое требуется сборщику мусора для освобождения «утечки» памяти. Чтобы понять степень влияния, изменим предыдущий скрипт путём добавления количества итераций и удаления промежуточных показателей потребления памяти. После изменения скрипт выглядит вот так:

Пример #2 Влияние на производительность

<?php

class Foo
{
public
$var = '3.14159265359';
public
$self;
}

for (
$i = 0; $i <= 1000000; $i++) {
$a = new Foo();
$a->self = $a;
}

echo
memory_get_peak_usage(), "\n";

?>

Запустим скрипт два раза: с включённой опцией zend.enable_gc и без неё.

Пример #3 Запуск скрипта

time php -dzend.enable_gc=0 -dmemory_limit=-1 -n example2.php
# и
time php -dzend.enable_gc=1 -dmemory_limit=-1 -n example2.php

На тестовой машине первая команда выполняется примерно 10.7 секунды, а вторая примерно 11.4 секунды. Это примерно на 7 % медленнее. Однако максимальное потребление памяти скриптом уменьшилось на 98 % — с 931 до 10 МБ. Этот тест производительности не научный и даже не представляет реальное приложение, но показывает преимущества в работе с памятью, которые даёт механизм сборки мусора. Хорошо то, что замедление скрипта каждый раз составляет одни и те же 7 %, тогда как экономия памяти постоянно увеличивается по мере того, как алгоритм во время выполнения скрипта обнаруживает всё больше циклических ссылок.

Внутренняя статистика сборщика мусора

PHP умеет выдавать больше информации о том, как механизм сборки мусора выполняется в PHP. Но для этого потребуется перекомпилировать PHP, чтобы включить код теста производительности и сбора данных. До запуска команды ./configure с параметрами, которые требуются пользователю, потребуется установить для переменной окружения CFLAGS значение -DGC_BENCH=1. Следующая последовательность должна сработать:

Пример #4 Пример перекомпиляции PHP для включения теста производительности сборки мусора

export CFLAGS=-DGC_BENCH=1
./config.nice
make clean
make

При повторном запуске приведенного примера кода с двоичным файлом PHP, который только что создали, после завершения выполнения PHP выведет следующее:

Пример #5 Статистика сборки мусора

GC Statistics
-------------
Runs:               110
Collected:          2072204
Root buffer length: 0
Root buffer peak:   10000

      Possible            Remove from  Marked
        Root    Buffered     buffer     grey
      --------  --------  -----------  ------
ZVAL   7175487   1491291    1241690   3611871
ZOBJ  28506264   1527980     677581   1025731

Самую информативную статистику показывает первый блок. Видно, что механизм сборки мусора запускался 110 раз, и суммарно освободил больше 2 миллионов записей в памяти. Как только механизм сборки мусора сработал хотя бы один раз, показатель пика корневого буфера (Root buffer peak) будет равняться 10 000.

Заключение

Сборщик мусора в PHP вызывает замедление работы только во время работы алгоритма сборки циклических ссылок, тогда как в стандартных скриптах меньшего размера производительность не падает.

Когда механизм сборки циклов все-таки запускается для стандартных скриптов, объём памяти, которую экономит механизм, разрешает одновременно запускать на сервере большее количество скриптов, поскольку в целом скрипты занимают не так много памяти.

Преимущества заметнее для скриптов, которые работают долго — большие наборы тестов или демоны. Новый механизм существенно сокращает утечки памяти для приложений, которые работают с расширением » PHP-GTK, которые часто выполняются дольше, чем скрипты для веба.