Перейти к основному содержимому

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.
  • Объекты эффективны: передача объектов обходится дёшево, так как передаётся только хэндл.

Источники и дальнейшее чтение​