Copy-on-Write (копирование при записи) в PHP
Copy-on-write (CoW, копирование при записи) — это оптимизация управления памятью в движке Zend, позволяющая избежать лишнего дублирования данных. Она гарантирует, что несколько переменных будут использовать одну и ту же область памяти до тех пор, пока одна из них не будет изменена.
- Shared Storage (общее хранилище): несколько переменных указывают на одну и ту же внутреннюю структуру (
zval), пока они выполняют только операции чтения. - Lazy Copying (ленивое копирование): выделение памяти под новую копию происходит только в момент модификации («при записи»).
Это поведение в первую очередь относится к массивам и строкам.
1. Управление памятью и передача по значению
Распространённое заблуждение гласит, что передача большого массива в функцию по значению всегда создаёт полную копию, что ведёт к высокому потреблению памяти.
function handle(array $array) {
// Здесь только чтение...
return count($array);
}
$largeArray = range(0, 500_000);
handle($largeArray);
В данном случае копия не создаётся. Параметр $array и переменная $largeArray разделяют один и тот же zval.
2. Модификация и триггер Copy-on-Write
Когда переменная изменяется, PHP проверяет, является ли её zval общим (refcount > 1). Если да, PHP «отщепляет» (separates) zval, создавая реальную копию для изменяемой переменной.
function handle(array $array) {
$array[0] = 'new value'; // Здесь срабатывает копирование
return $array;
}
Что вызывает копирование?
- Изменение элемента массива.
- Добавление или удаление элементов (
unset()). - Изменение символа в строке.
- Сортировка массива.
3. Замер пикового потребления памяти
Следующий пример демонстрирует, как пиковое потребление памяти остаётся стабильным при чтении, но резко возрастает при модификации.
<?php
function printMemory(string $header): void
{
echo "{$header}: " . round(memory_get_peak_usage() / 1024) . " KB" . PHP_EOL;
}
function handleRead(array $array): void
{
foreach ($array as $row) {
$dummy = $row;
}
printMemory('Пик внутри handleRead');
}
function handleWrite(array $array): void
{
$array[0] = 'changed';
printMemory('Пик внутри handleWrite');
}
$largeArray = range(0, 500_000);
printMemory('Начальный пик');
handleRead($largeArray);
handleWrite($largeArray);
Ожидаемые результаты (приблизительно):
- Начальный пик: 16 764 KB
- Пик внутри handleRead: 16 764 KB (Копирования нет)
- Пик внутри handleWrite: 33 153 KB (Создана копия)
4. Объекты и хэндлы (handles)
Объекты в PHP 5+ не используют механизм copy-on-write для своих свойств так же, как массивы. При передаче объекта PHP передаёт хэндл объекта (идентификатор).
function handle(stdClass $obj) {
$obj->property = 'new value'; // Изменяет оригинальный объект
}
Модификация свойства объекта внутри функции влияет на оригинальный объект, так как обе переменные указывают на один и тот же экземпляр через один и тот же хэндл. Следовательно, автоматического дублирования всей структуры объекта при модификации не происходит.
5. Практические выводы
- Избегайте ненужных ссылок: не используйте
&$arrayтолько ради «оптимизации». CoW в PHP эффективно справляется с памятью при доступе на чтение. Используйте ссылки только тогда, когда вам действительно нужно изменить оригинальную переменную. - Осторожнее с большими массивами: если вы работаете с огромными наборами данных и вам нужно их изменять, рассмотрите альтернативы, такие как
GeneratorsилиSplFixedArray. - Объекты эффективны: передача объектов обходится дёшево, так как передаётся только хэндл.
Источники и дальнейшее чтение
- Copy-on-write в PHP — Александр Зубарев (2021).
- PHP Internals Book — Memory Management
- PHP Manual — Objects and References