Структура zval занимает шестнадцать байт: объединение (union) значения и тег типа. То, что записано в этом теге, определяет, копирует ли что-то присваивание, во сколько обходится ссылка и какая переменная за это платит.
Копирование массива из миллиона элементов во вторую переменную изменяет значение memory_get_usage() на ноль байт. Запись одного элемента в любую из двух переменных увеличивает его на 16 793 680 — весь массив списывается на то имя, которое выполнило запись первым.
<?php
declare(strict_types=1);
$baseline = memory_get_usage();
$rows = range(0, 999_999);
printf("build %+d bytes\n", memory_get_usage() - $baseline);
$baseline = memory_get_usage();
$copy = $rows;
printf("second name %+d bytes\n", memory_get_usage() - $baseline);
$baseline = memory_get_usage();
$copy[0] = 1;
printf("first write %+d bytes\n", memory_get_usage() - $baseline);
$baseline = memory_get_usage();
$copy[1] = 1;
printf("second write %+d bytes\n", memory_get_usage() - $baseline);build +16793680 bytes
second name +0 bytes
first write +16793680 bytes
second write +0 bytesДве записи, один счет. В самом массиве между ними ничего не изменилось, и в исходном коде между ними нет никаких различий. Изменилось число, которое находится рядом с массивом, а не внутри какой-либо из переменных. И сами переменные — это лучшее место для начала, потому что они меньше, чем кажутся.
Как снимались эти показатели
PHP 8.5.10, NTS, arm64, сборка Homebrew, ноутбук Apple M4 Pro с 24 ГБ ОЗУ и 12 логическими ядрами под управлением macOS (без перевода системы в фоновый режим). OPcache и JIT отключены для всех приведенных ниже цифр, а memory_limit увеличен до 2G, чтобы поместились сценарии с миллионом элементов — за исключением одного места ниже, где указан другой лимит (в чем и заключается суть того измерения). Остальная часть архива измерялась на PHP 8.5.9 на той же машине.
Показатели памяти представляют собой одиночные точные измерения, а не медианы: два последовательных запуска всего скрипта дали идентичный с точностью до байта результат, поэтому отклонения отсутствуют. Временные замеры — это медиана 15 чередующихся запусков без учета трех прогревочных запусков, снятая с помощью hrtime() внутри процесса; диапазон указан для каждого случая. Когда в таблице замеряется время выполнения одного выражения из последовательности, таймер охватывает только это выражение, а подготовка вокруг него выполняется без замеров. Считывайте временные показатели как относительные по отношению друг к другу, а не как абсолютные для вашего железа — причины описаны в статье как тестировать производительность PHP, не обманывая себя.
Шестнадцать байт, что бы вы в них ни положили
Переменная на уровне пользовательского кода — это zval, и в 64-битной сборке она занимает шестнадцать байт: восьмибайтовое объединение (union), хранящее значение, четырехбайтовое слово с тегом типа и его флагами, и еще четыре байта, которые движок использует для внутренних нужд вроде указателя на следующий элемент в цепочке коллизий хэша. Объединение имеет ширину в одно машинное слово, поэтому целое число (integer) или число с плавающей точкой (float) хранятся прямо внутри него, в то время как строка, массив или объект представляют собой указатель на другую структуру.
Имени переменной там нет. Компилятор назначает каждой именованной локальной переменной пронумерованный слот в фрейме вызова (call frame) — это компилируемая переменная, которую дамп опкодов выводит как CV0, — и шестнадцать байт составляют содержимое этого слота.
Тот факт, что слот никогда не меняет размер, можно измерить из пользовательского кода, поскольку упакованный массив (packed array) — это просто ряд zval-структур и ничего более: внутреннее устройство любого массива в PHP хранит значение и вычисляет ключ исходя из его позиции. Заполнение такого массива миллионом значений четырех разных типов:
appended 1,000,000 integers 16.79 bytes/element appended 1,000,000 floats 16.79 bytes/element appended 1,000,000 booleans 16.79 bytes/element appended 1,000,000 nulls 16.79 bytes/element
Четыре типа, одна цифра, с точностью до байта. Получилось 16.79, а не 16.00, потому что массив выделил емкость под 1 048 576 элементов плюс заголовок поверх этого; сохранение 1 048 576 целых чисел вместо миллиона дало те же самые 16 793 680 байт, что представляет собой 1 048 576 слотов по шестнадцать байт плюс 16 464 байта таблицы.
Null — это случай, на котором стоит остановиться подробнее. null — это тег типа без полезной нагрузки, но он все равно стоит целого слота, потому что именно слот представляет собой элемент массива. То же самое справедливо для true и false: у движка есть для них отдельные теги типов, поэтому булево значение даже не использует объединение значений.
Счетчик находится в значении, а не в переменной
Один байт из этого четырехбайтового слова — это тег типа, а еще один содержит флаги. Один единственный флаг определяет все вышеописанное: учитывается ли количество ссылок (reference counted) для сущности, на которую указывает объединение. Для integers, floats, booleans и null ссылки не считаются, потому что указывать не на что. Для строк, массивов и объектов ссылки считаются, и их счетчик хранится в восьмибайтовом заголовке адресуемой структуры, а не в переменной.
Комментарии (0)
Пока нет комментариев — будьте первым.