Производительность

Основы подсчета ссылок

Переменная PHP хранится в контейнере, называемом "zval". Контейнер zval, помимо типа и значения переменной, также содержит два дополнительных элемента. Первый называется "is_ref" и представляет булево значение, указывающее, является переменная частью "набора ссылок" или нет. Благодаря этому элементу PHP знает как отличать обычные переменные от ссылок. Так как PHP содержит пользовательские ссылки, которые можно создать оператором &, контейнер zval также содержит внутренний механизм подсчета ссылок для оптимизации использования памяти. Эта вторая часть дополнительной информации, называемая "refcount" (счетчик ссылок), содержит количество имен переменных (также называемых символами), которые указывают на данный контейнер zval. Все имена переменных хранятся в таблице имен, отдельной для каждой области видимости переменных. Такая область видимости существует для главного скрипта, а также для каждой функции и метода.

Контейнер zval создается при создании новой переменной, которой присваивается константа, например:

<?php
$a = "new string";
?>

В данном примере создается новый символ a в текущей области видимости и новый контейнер переменной с типом string и значением new string. Бит is_ref по умолчанию задается равным FALSE, т.к. не создано ни одной пользовательской ссылки. Значение же refcount задается равным 1, т.к. только одно имя переменной указывает на данный контейнер. Отметим, что если refcount равен 1, то is_ref будет всегда равен FALSE. Если у вас установлен Xdebug, то вы можете вывести эту информацию, вызвав функцию xdebug_debug_zval().

<?php
xdebug_debug_zval('a');
?>

Результат выполнения данного примера:

a: (refcount=1, is_ref=0)='new string'

Присвоение этой переменной другой увеличивает счетчик ссылок:

<?php
$a = "new string";
$b = $a;
xdebug_debug_zval( 'a' );
?>

Результат выполнения данного примера:

a: (refcount=2, is_ref=0)='new string'

Счетчик ссылок здесь равен 2, т.к. a и b ссылаются на один и тот же контейнер переменной. PHP достаточно умен, чтобы не копировать контейнер, пока в этом нет необходимости. Как только refcount станет равным нулю, контейнер уничтожается. refcount уменьшается на единицу при уходе переменной из области видимости (например, в конце функции) или при вызове unset() с данной переменной.

Сбор циклических ссылок

Начиная с версии 5.3.0, в PHP реализован синхронный механизм подсчета ссылок в памяти. По умолчанию сборщик мусора всегда включен. Для изменения этой опции используется параметр zend.enable_gc в php.ini.

Если сборщик мусора включен, алгоритм поиска циклических ссылок выполняется каждый раз, когда корневой буфер наполняется 10,000 корнями (вы можете поменять это значение, изменив константу GC_ROOT_BUFFER_MAX_ENTRIES в файле Zend/zend_gc.c в исходном коде PHP и пересобрав PHP). Если сборщик мусора выключен, алгоритм никогда не будет запущен. Тем не менее, буфер всегда заполняется корнями.

Если буфер заполнился при выключенном механизме сборки мусора, то другие корни не будут в него записаны. Таким образом, если они окажутся мусором с циклическими ссылками, то никогда не будут очищены и создадут утечку памяти.

Помимо изменения параметра zend.enable_gc, механизм сборки мусора также можно запустить и остановить вызвав функции gc_enable() и gc_disable() соответственно. Вызов этих функций имеет тот же эффект, что и включение/выключение механизма с помощью настроек конфигурации. Кроме того, можно запустить сборку мусора, даже если корневой буфер еще не заполнен. Для этого вы можете вызвать функцию gc_collect_cycles(), которая также возвращает количество циклических ссылок собранных алгоритмом.

Уменьшение размера используемой памяти

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

<?php
class Foo
{
    public $var = '3.14159265359';
}

$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";
    }
}
?>

В этом очень академическом примере мы создаем объект, свойство a которого задается ссылкой на сам объект. Когда в скрипте в следующей итерации цикла переопределяется переменная $a, то происходит типичная утечка памяти. В данном случае пропадают два контейнера zval (контейнер объекта и контейнер свойства объекта), но определяется только один корень - удаленная переменная. Как только пройдут 10 000 итераций (максимально в корневом буфере будет 10 000 корней), то запустится механизм сборки мусора и память, занимаемая этими корнями, будет освобождена.

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

Есть две основные области влияющие на производительность: уменьшение размера используемой памяти и замедление работы при сборке мусора.

Уменьшение размера используемой памяти

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

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

Второй областью, где сборка мусора влияет на производительность, является потеря времени, когда сборщик мусора освобождает память.

Заключение

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

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

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

Кэширование кода OPcache

Производительность скриптов может быть значительно увеличена. Процесс выполнения PHP скрипта состоит из двух частей: сначала PHP скрипт компилируется в машинный код, а потом этот машинный код выполняется.

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

Однажды скомпилированный машинный код скрипта хранится в оперативной памяти.

opcache_compile_file()

  • Компилирует и кэширует PHP скрипт без выполнения его.

opcache_get_configuration()

  • Возвращает информацию о конфигурации кэша.

opcache_get_status()

  • Возвращает информацию о статусе кэша.

opcache_invalidate($script, $force = FALSE)

  • Удаляет из кэша данные о скрипте.
  • Если $force выставлен в FALSE, то файл будет удален из кэша только если его дата изменения новее, чем дата в кэше.

opcache_is_script_cached()

  • Проверяет кашерирован ли скрипт.

opcache_reset()

  • Сбрасывает содержимое кэша.

APC (Alternative PHP Cache)

Открытый с исходными кодами кэш для PHP. Основная цель - предоставить открытый, свободный и надежный фреймворк для кэширования и отпимизации промежуточного кода PHP.