ZVAL в PHP: углублённый анализ работы с переменными. Часть 2 — копирование, ссылки и оптимизация

Содержание

Введение

В предыдущей части мы рассмотрели, что такое ZVAL в PHP. В это части более подробно рассмотрим, копирование, ссылки и возможные оптимизации.

Все примеры приведены для php 8.3. Для отладки примеров будем использовать метод xdebug_debug_zval. Для этого должно быть установлено расширение xdebug.

$var = 1;
xdebug_debug_zval('var');

Почему мы используем xdebug_debug_zval() вместо debug_zval_dump()?

Начиная с PHP 8.0, функция debug_zval_dump() перестала отображать критически важную информацию:

  • Не показывает refcount (счетчик ссылок)
  • Не отображает is_ref (флаг ссылочности)

В то время как xdebug_debug_zval() продолжает предоставлять полную информацию о внутренней структуре ZVAL:

$a = [1, 2, 3];
xdebug_debug_zval('a');
// Вывод: (refcount=2, is_ref=0)=array(...)

debug_zval_dump($a);
// Вывод PHP 8+: array(3) { ... } - без ключевых метаданных

Примечание: Функция xdebug_debug_zval() всегда показывает значение refcount на 1 больше реального. Это особенность реализации Xdebug — при выводе информации он временно увеличивает счетчик ссылок. В следующих примерах я буду указывать корректные значения refcount (уменьшенные на 1), чтобы отражать реальное состояние переменных.

Пример вывода:

$a = [1,2,3];
xdebug_debug_zval('a'); 
// Выведет: (refcount=2, is_ref=0)=...
// Реально: refcount = 1

Реализациия метода на xdebug_debug_zval С.

void xdebug_debug_zval(char *varname) {
    zval *zv;
    // вот здесь +1 временная ссылка
    zv = xdebug_get_zval(varname); 
    php_printf("%s: (refcount=%d, is_ref=%d)=...", 
        varname, 
        Z_REFCOUNT_P(zv) + 2,
        Z_ISREF_P(zv));
}

1. Разбор Copy-on-Write

Механизм Copy-on-Write (COW) — фундаментальная оптимизация в PHP, которая минимизирует использование памяти:

// Исходный массив (refcount=1 в реальности, xdebug покажет +1)
$original = [1];
xdebug_debug_zval('original');
// Вывод Xdebug: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1)
// Реальный refcount = 1

// Присваивание (refcount увеличивается до 2)
$copy = $original;
xdebug_debug_zval('original');
// Вывод: (refcount=3, is_ref=0)=...
// Реальный refcount = 2
xdebug_debug_zval('copy');
// Вывод: (refcount=3, is_ref=0)=...
// Реальный refcount = 2 (общая ссылка)

// Модификация (срабатывает COW). Для $copy создается новый zval.
$copy[0] = 999;
xdebug_debug_zval('original');
// Вывод: (refcount=2, is_ref=0)=array (0 => (refcount=0, is_ref=0)=1)
// Реальный refcount = 1 (после разделения)

xdebug_debug_zval('copy');
// Вывод: (refcount=1, is_ref=0)=array (0 => (refcount=0, is_ref=0)=999)
// Реальный refcount = 0 (новая копия)

Обратите внимание на последний вывод

xdebug_debug_zval('copy'); 
// Реальный refcount = 0 (новая копия)

Почему refcount = 0? Подробнее читайте в статье.

Ключевые аспекты COW:

  • Глубокое копирование происходит только при модификации
  • Исключения: Объекты (PHP 5+) всегда передаются по ссылке
  • Особенность PHP 8.1: Оптимизация для массивов с одним элементом

2. Руководство по ссылкам

Ссылки в PHP — это не указатели, а особый флаг is_ref в ZVAL:

$a = 10;
xdebug_debug_zval('a');
// a: (refcount=0, is_ref=0)=10

// Устанавливается is_ref=1
$b = &$a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
// a: (refcount=2, is_ref=1)=10
// b: (refcount=2, is_ref=1)=10

// При is_ref=1 создаётся НОВЫЙ zval
$c = $a;
xdebug_debug_zval('a');
xdebug_debug_zval('b');
xdebug_debug_zval('c');
// a: (refcount=2, is_ref=1)=10
// b: (refcount=2, is_ref=1)=10
// c: (refcount=0, is_ref=0)=10

PHP следует этим правилам при работе с ссылками:

  1. Если есть хотя бы одна жёсткая ссылка (is_ref=1):
    • Все новые присваивания по значению (=) создают копии
    • Это предотвращает неожиданные изменения связанных переменных
  2. Скалярные значения (числа, строки):
    • При копировании используют оптимизацию interned-хранилища
    • Поэтому $c получает refcount=0

Важные нюансы:

  • Цена ссылок: Увеличивают потребление памяти на 30-40%
  • Неожиданное поведение: При изменении через ссылку меняются все связанные переменные
  • Оптимизация PHP 8: Сокращение накладных расходов на ссылки

3. Детали изменения типов

Пример работы с zval и преобразованием типов

$var1 = "42";      // type=IS_STRING
xdebug_debug_zval('var1');
// var1: (interned, is_ref=0)='42'

$var1 += 0;        // Неявное преобразование в IS_LONG
xdebug_debug_zval('var1');
// var1: (refcount=0, is_ref=0)=42

settype($var1, 'float'); // Явное изменение типа
xdebug_debug_zval('var1');
// var1: (refcount=1, is_ref=1)=42

// Особый случай. Пустая строка преобразуется в false
$var2 = "0";       
xdebug_debug_zval('var2');
// var2: (interned, is_ref=0)='0'

Разбор примера:

  1. Изначально $var1 содержит строку (IS_STRING) с флагом interned
  2. При арифметической операции происходит неявное преобразование в целое число (IS_LONG)
  3. settype() выполняет явное преобразование в тип float
  4. Особый случай: строка «0» не считается пустой, но в булевом контексте преобразуется в false

Внутренние механизмы:

  • Хэширование: Для быстрого сравнения смешанных типов
  • Кэширование: Сохранение преобразованных значений
  • Опасности: Потеря точности при больших числах

4. Оптимизация памяти

Профессиональные техники работы с памятью:

МетодЭффектПример
unset()Немедленное освобождениеunset($largeArray)
= nullОтложенное освобождение$var = null
Ссылки+30% к памяти$ref = &$original

5. Практические кейсы

Кейс 1: Оптимизация обработки больших данных

// Проблемный код:
function processData() {
    $data = loadHugeDataset(); // 500MB
    modifyData($data);
    return $data;
}

// Решение:
function processDataOptimized() {
    $data = loadHugeDataset();
    modifyData($data);
    unset($data); // Явное освобождение
    return $result;
}

Больше примеров вынес в отдельную статью.

6. Интернирование строк

PHP автоматически оптимизирует хранение одинаковых строк:

$a = 'hello';
$b = 'hello';
debug_zval_dump($a); // string(5) "hello" interned
debug_zval_dump($b); // string(5) "hello" interned

Это означает, что в памяти хранится только одна копия строки, а все переменные ссылаются на один zval.

7. Измерение потребления памяти

<?php

function test1(): void
{
	$data = range(1, 100000);

	$memBefore = memory_get_usage();
	$copy = $data;
	$memAfter = memory_get_usage();

	showMemory($memBefore, $memAfter);
}

function test2(): void
{
	$data = range(1, 100000);

	$memBefore = memory_get_usage();
	$copy = $data;
	$copy[0] = 1;
	$memAfter = memory_get_usage();

	showMemory($memBefore, $memAfter);
}

function test3(): void
{
	$data = range(1, 100000);

	$memBefore = memory_get_usage();
	$copy = &$data;
	$memAfter = memory_get_usage();

	showMemory($memBefore, $memAfter);
}

function test4(): void
{
	$data = range(1, 100000);

	$memBefore = memory_get_usage();
	$copy = &$data;
	$copy[0] = 1;
	$memAfter = memory_get_usage();

	showMemory($memBefore, $memAfter);
}

function showMemory($memBefore, $memAfter): void
{
	echo "Память до копирования: {$memBefore} байт\n";
	echo "Память после копирования: {$memAfter} байт\n";
	echo "Разница: " . ($memAfter - $memBefore) . " байт\n";
}

// 1. Присвоение одного массива другому. Без изменения данных.
test1();
// Память не увеличилась.
// Память до копирования: 2514944 байт
// Память после копирования: 2514944 байт
// Разница: 0 байт

// 2. Присвоение одного массива другому. С изменением данных.
test2();
// В результате память увеличилась в двое, т.к. массив был скопирован.
// Память до копирования: 2514976 байт
// Память после копирования: 4628592 байт
// Разница: 2113616 байт

// 3. Присвовение массива по ссылке, без изменения данных
test3();
// Выделяется память только на ссылку.
// Память до копирования: 2514976 байт
// Память после копирования: 2515008 байт
// Разница: 32 байт

// 4. Присвовение массива по ссылке, с изменением данных
test4();
// Выделяется память только на ссылку.
// Память до копирования: 2514976 байт
// Память после копирования: 2515008 байт
// Разница: 32 байт

Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *