Пользователь
0,0
рейтинг
18 декабря 2011 в 03:47

Разработка → Работа с памятью (и всё же она есть)

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


Общие сведения


Небольшой ликбез

Переменная в PHP как бы состоит из двух частей: "имени", которое хранится в hash_table symbol_table, и "значения", которое хранится в zval контейнере.
Такой механизм позволяет создавать несколько переменных ссылающихся на одно значение, что в отдельных случаях позволяет оптимизировать потребление памяти. О том, как это выглядит на практике будет написано далее.

Наиболее частыми элементами кода, без которых сложно себе представить более менее функциональный скрипт, являются следующие моменты:
— создание, присвоение и удаление переменных (чисел, строк и т.п.),
— создание массивов и их обход (в качестве примера будет использована функция foreach),
— передача и возврат значений для функций/методов.

Именно об этих аспектах работы с памятью и будет последующее описание. Получилось достаточно объемно, но ничего мега-сложного не будет и всё будет достаточно просто, очевидно и с примерами.

Первый пример работы с памятью

Для начала базовый пример того, как будет производиться анализ потребления памяти.
Для этого нам потребуется пара простых функций (файл func.php):
<?php
function memoryUsage($usage, $base_memory_usage) {
printf("Bytes diff: %d\n", $usage - $base_memory_usage);
}
function someBigValue() {
return str_repeat('SOME BIG STRING', 1024);
}
?>


И простой первый пример теста потребления памяти для строки:
<?php
include('func.php');
echo "String memory usage test.\n\n";
$base_memory_usage = memory_get_usage();
$base_memory_usage = memory_get_usage();
 
echo "Start\n";
memoryUsage(memory_get_usage(), $base_memory_usage);
 
$a = someBigValue();
 
echo "String value setted\n";
memoryUsage(memory_get_usage(), $base_memory_usage);
 
unset($a);
 
echo "String value unsetted\n";
memoryUsage(memory_get_usage(), $base_memory_usage);
?>

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

Результат кода вполне очевиден:
String memory usage test.

Start
Bytes diff: 0
String value setted
Bytes diff: 15448
String value unsetted
Bytes diff: 0


Тот же самый пример, но вместо unset($a) используем $a=null;:
Start
Bytes diff: 0
String value setted
Bytes diff: 15448
String value set to null
Bytes diff: 76

Как видите, переменная не была полностью уничтожена. Под нее остается выделенным еще 76 байт.
Достаточно прилично, если учесть, что ровно столько же выделяется и под переменные типа boolean, integer, float. Речь идет не об объеме памяти, выделяемой под значение переменной, а о полном потреблении памяти для хранения сведений о присвоенной переменной (zval контейнер со значением и само имя переменной).
Так что если вы хотите освободить память при помощи присвоения, то не является принципиальным присвоение именно null значения. Выражение $a=10000; даст тот же результат для расхода памяти.

В документации PHP сказано, что приведение к null уничтожит переменную и ее значение, однако, по данному скрипту видно что это не так, что собственно является багом (документации).

Зачем использовать присвоение null, если можно unset()?
Присвоение — это присвоение, (спасибо КО), то есть изменяется значение переменной, соответственно, если новое значение требует меньше памяти, то она высвобождается сразу, однако это требует вычислительных ресурсов (пусть и сравнительно немного).
unset() в свою очередь освобождает память, выделенную под имя переменной и ее значение.
Отдельно стоит упомянуть момент, что unset() и присвоение null совершенно по разному работают со ссылками на переменные. Unset() уничтожит только ссылку, в то время как присвоение null изменит значение, на которое ссылаются имена переменных, соответственно все переменные станут ссылаться на значение null.

Примечание:
Встречается заблуждение, что unset() является функцией, однако, это не верно. unset() — это языковая конструкция (как например if), о чем прямо сказано в документации, соответственно ее нельзя использовать для обращения через значение переменной:
$unset_func_name = 'unset';
$unset_func_name($some_var);


Немного дополнительной информации для праздных размышлений (при изменении примера выше):
$a = array();
выделит 164 байта, unset($a) всё вернет.

class A { }
$a = new A();
выделит 184 байта, unset($a) всё вернет.

$a = new stdClass();
выделит 272 байта, но после unset($a) «утекут» 88 байт (куда именно и почему они утекли, мне пока не удалось выяснить).

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

Массивы


Массивы в PHP «съедают» достаточно памяти, и именно в них как правило хранят значительные объемы данных при обработке, поэтому следует очень аккуратно относиться к работе с ними. Однако, работа с массивами в PHP имеет свои «прелести оптимизации» и об одном из таких моментов, связанных с потреблением памяти, стоит упомянуть.

Коварный пример №1
<?php
include('func.php');
echo "Array memory usage example.";
$base_memory_usage = memory_get_usage();
$base_memory_usage = memory_get_usage();
 
echo 'Base usage.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
$a = array(someBigValue(), someBigValue(), someBigValue(), someBigValue());
 
echo 'Array is set.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
foreach ($a as $k=>$v) {
$a[$k] = someBigValue();
unset($k, $v);
echo 'In FOREACH cycle.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
}
 
echo 'Usage right after FOREACH.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
unset($a);
echo 'Array unset.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
?>

На первый взгляд может показаться, что потребление памяти массивом $a не будет меняться (за исключением установки переменных $k и $v), однако PHP имеет особенный подход при работе с массивами в этом случае.

Посмотрите на вывод:
Array memory usage example.Base usage.
Bytes diff: 0
Array is set.
Bytes diff: 61940
In FOREACH cycle.
Bytes diff: 77632
In FOREACH cycle.
Bytes diff: 93032
In FOREACH cycle.
Bytes diff: 108432
In FOREACH cycle.
Bytes diff: 123832
Usage right after FOREACH.
Bytes diff: 61940
Array unset.
Bytes diff: 0

Получается, что в последней итерации цикла foreach в данном случае потребление массивом памяти возросло в два раза, хотя по самому коду это не очевидно. Но сразу после цикла, потребление памяти вернулось к прежнему значению. Чудеса да и только.
Причиной тому является оптимизация использования массива в цикле. На время работы цикла, при попытке изменить исходный массив, неявно создается копия структуры массива (но не копия значений), которая и становится доступной по завершению цикла, а исходная структура уничтожается. Таким образом, в вышеприведенном примере, если вы присваиваете новые значения исходному массиву, то они не будут заменены сразу, а для них будет выделена отдельная память, которая будет возвращена по выходу из цикла.
Этот момент очень легко пропустить, что может привести к значительному потреблению памяти на время работы цикла с большими массивами данных, например при выборке из БД.

Примечание:
Внутри самого цикла, уже после изменения значения $a[$k], вы не сможете получить значение которое всё еще хранится в исходном массиве если не сохранили значение $v. Повторное обращение к $a[$k] выдаст уже новое значение.

Дополнение от пользователя zibada (в кратце):
Важно учесть, что выделение памяти под новый «временный массив» в случае внесения изменений, произойдет единовременно для всей структуры массива, но отдельно для каждого изменяемого элемента. Таким образом, если имеется массив с большим количеством элементов, (но не обязательно с большими значениями), то единовременное потребление памяти при таком копировании будет существенно.

Коварный пример №2
Чуть-чуть изменим код.
echo 'Array is set.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
$b = &$a; // Добавим это
foreach ($a as $k=>$v) {
$a[$k] = someBigValue();
unset($k, $v);
echo 'In FOREACH cycle.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
}
unset($b); // И это
echo 'Usage right after FOREACH.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);


Сам код цикла мы никак не меняли, единственное что мы изменили, это увеличили счетчик ссылок на исходный массив, но это в корне поменяло работу цикла:
Bytes diff: 0
Array is set.
Bytes diff: 61940
In FOREACH cycle.
Bytes diff: 61988
In FOREACH cycle.
Bytes diff: 61988
In FOREACH cycle.
Bytes diff: 61988
In FOREACH cycle.
Bytes diff: 61988
Usage right after FOREACH.
Bytes diff: 61940
Array unset.
Bytes diff: 0

Небольшое изменение: (61988 — 61940 = 48 байт на хранение переменной-ссылки $b).
В остальном же мы видим, что если массив, используемый для цикла, имеет больше чем одну ссылку на себя, тогда для него не применяется оптимизация из примера №1, т.е. для присвоения используется оригинальный массив.
Точно такой же результат мы получим, если используем для цикла массив $b или же используем в цикле передачу значения по ссылке:
echo 'Array is set.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
foreach ($a as $k=>&$v) {
$a[$k] = someBigValue(); // Или $v = someBigValue();
unset($k, $v);
echo 'In FOREACH cycle.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
}
 
echo 'Usage right after FOREACH.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);


Результат:
Bytes diff: 0
Array is set.
Bytes diff: 61940
In FOREACH cycle.
Bytes diff: 61940
In FOREACH cycle.
Bytes diff: 61940
In FOREACH cycle.
Bytes diff: 61940
In FOREACH cycle.
Bytes diff: 61940
Usage right after FOREACH.
Bytes diff: 61940
Array unset.
Bytes diff: 0

Здесь стоит отдельно отметить, что добавление передачи $v по ссылке хоть и не увеличивает счетчик ссылок исходного массива, но тоже приводит к отключению «оптимизации».

Передача по ссылке или передача через копирование



Рассмотрим случай, «что делать» если требуется передать в метод или функцию (или вернуть из них), какое-либо очень большое значение. Первым очевидным решением обычно рассматривают использование передачи/возвращения по ссылке.
Однако в документации по PHP сказано: Не используйте возврат по ссылке для увеличения производительности. Ядро PHP само занимается оптимизацией.
Попытаемся разобраться в том, что же это за «оптимизация».

Для начала самый простой пример (пока без передачи аргументов):

$a = someBigValue();
$b = $a;
 
echo "String value setted";
memoryUsage(memory_get_usage(), $base_memory_usage);
 
unset($a, $b);
...

По «прямой логике», в памяти должно выделиться два блока под значение переменных. Однако PHP оптимизирует этот момент:
Start
Bytes diff: 0
String value setted
Bytes diff: 15496
String value unsetted
Bytes diff: 0

В данном случае 15448 байт занимается переменная $a, остальные же 48 байт выделены под переменную $b, хотя между ними и не установлена связь по ссылке. Данное потребление памяти сохраняется до тех пор, пока мы как-либо не изменим одну из этих переменных, а точнее сказать вообще что-либо не сделаем с ее значением, даже если мы его не меняем по факту:
$a = someBigValue();
$b = $a;
$b = strval($b);
 
echo "String value setted";
memoryUsage(memory_get_usage(), $base_memory_usage);
 
unset($a, $b);


В результате получим вывод:
Bytes diff: 0
String value setted
Bytes diff: 30896
String value unsetted
Bytes diff: 0

Как мы видим, попытка «тронуть» значение переменной $b приводит к тому, что теперь скрипт выделяет для ее хранения отдельную область памяти. То же самое произойдет если мы попытаемся «тронуть» значение $a.

Данная оптимизация действует для конкретных значений, коими также являются и отдельные значения массива.
Чтобы это лучше понять, взглянем на пример ниже:
$a = array(someBigValue(), someBigValue()); // 31052 байта
$b = $a; // + 48 байт = 31100 байта
$b[0] = someBigValue();
 
echo "String value setted";
memoryUsage(memory_get_usage(), $base_memory_usage);
 
unset($a, $b);


Данный пример даст выход:
Bytes diff: 0
String value setted
Bytes diff: 46704
String value unsetted
Bytes diff: 0

То есть в результате новая память (15к+ байт) была выделена для создания только копии значения для нулевого элемента массива, а не для всего массива $b. Значение $b[1] всё еще «оптимизированно связано» с $a[1].

Всё выше описанное действует аналогично и для передачи/возврата значений через «оптимизированное копирование» внутрь/из функций и методов. Если внутри метода вы никак не «трогаете» переданное значение, то для него не будет выделена отдельная область памяти (память будет выделена только под имя переменной, чтобы связать ее со значением). Если же вы передаете «через копирование» и изменяете значение внутри метода, то перед попыткой сделать изменение уже будет создана действительная полная копия значения.

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

Код для примера:
<?php
include('func.php');
 
function testUsageInside($big_value, $base_memory_usage) {
echo 'Usage inside function then $big_value NOT changed.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
$big_value[0] = someBigValue();
echo 'Usage inside function then $big_value[0] changed.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
$big_value[1] = someBigValue();
echo 'Usage inside function then also $big_value[1] changed.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
}
 
echo "Array memory usage example.";
$base_memory_usage = memory_get_usage();
$base_memory_usage = memory_get_usage();
 
echo 'Base usage.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
$a = array(someBigValue(), someBigValue(), someBigValue(), someBigValue());
 
echo 'Array is set.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
testUsageInside($a, $base_memory_usage);
 
echo 'Usage right after function call.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
 
unset($a);
echo 'Array unset.'.PHP_EOL;
memoryUsage(memory_get_usage(), $base_memory_usage);
?>


Вывод:
Array memory usage example.
Base usage.
Bytes diff: 0
Array is set.
Bytes diff: 61940
Usage inside function then $big_value NOT changed.
Bytes diff: 61940
Usage inside function then $big_value[0] changed.
Bytes diff: 77632
Usage inside function then also $big_value[1] changed.
Bytes diff: 93032
Usage right after function call.
Bytes diff: 61940
Array unset.
Bytes diff: 0

Как видно из примера, в функции не была создана копия массива, несмотря на то, что фактически идет передача значения через копирование. И даже частичная модификация переданного массива не создала полноценную копию, а выделила память только под новые значения.

Исключительно в познавательных целях, стоит обратить внимание на эти два значения:
Array is set.
Bytes diff: 61940
Usage inside function then $big_value NOT changed.
Bytes diff: 61940

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

Примечание:
В PHP5 (в отличие от PHP4), все объекты по-умолчанию передаются по ссылке, хотя по факту, это неполноценная ссылка. См. эту статью.

Краткие выводы


Несомненно приведенные примеры оптимизации использования памяти в PHP лишь «капля в море», однако они описывают самые частые случаи, когда имеет смысл задуматься о том, какой код выбрать чтобы оптимизировать расход памяти и избавить себя от лишней головной боли.

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

PS: Можно было бы разбить это на несколько статей, но не вижу в этом смысла, так как подобную информацию лучше всё же хранить «вместе». Полагаю тем, кому данная информация несет практический смысл, так будет удобнее. Тестировалось на PHP 5.3.2 (Ubuntu 32bit), так что ваши значения по выделенным байтам могут отличаться.

Еще много полезного, но на английском:
nikic.github.com/2011/12/12/How-big-are-PHP-arrays-really-Hint-BIG.html
nikic.github.com/2011/11/11/PHP-Internals-When-does-foreach-copy.html
blog.golemon.com/2007/01/youre-being-lied-to.html
hengrui-li.blogspot.com/2011/08/php-copy-on-write-how-php-manages.html
sldn.softlayer.com/blog/dmcaloon/PHP-Memory-Management-Foreach
blog.preinheimer.com/index.php?/archives/354-Memory-usage-in-PHP.html
derickrethans.nl/talks/phparch-php-variables-article.pdf

UPD
В основной части статьи не был освещен важный момент.
Если есть переменная на которую создана ссылка, то при ее передаче в функцию в качестве аргумента она будет скопирована сразу, то есть не будет применена copy-on-write оптимизация.
Пример:
<?php
include('func.php');
function testFunc($a, $base_memory_usage) {
memoryUsage(memory_get_usage(), $base_memory_usage);
}
$base_memory_usage = 0;
$base_memory_usage = memory_get_usage();
memoryUsage(memory_get_usage(), $base_memory_usage); // 0 bytes
$a = someBigValue();
$b = &$a;
memoryUsage(memory_get_usage(), $base_memory_usage); // 15496 bytes
testFunc($a, $base_memory_usage); // 30896 bytes
memoryUsage(memory_get_usage(), $base_memory_usage); // 15496 bytes
unset($a, $b);
memoryUsage(memory_get_usage(), $base_memory_usage); // 0 bytes
?>
 
Евгений @evgenyl
карма
178,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (90)

  • –82
    That's it, I'm moving to Python =)
    • +33
      и смысл таких комментариев?
      • +12
        Тролль такой тролль
      • –13
        забыл irony вставить.
        • 0
          Поздно.
          • –12
            Нее, нармально
    • –3
      Очень тонко.
    • –3
      Пистон малопригоден для целей, для которых предназначен PHP
      • –5
        Да, ломать разработчику мозг, путая извилины, и приучая говнокодить путем постоянной демонстрации мегатонн уже написанных спагетти, питон действительно непригоден.
        • +1
          Ок, бро
        • 0
          Я и на питоне в джанге такое нарисовать мог, что тру-пайтонистов инфаркт хватит. Архитектура приложения от языка, имхо, мало зависит, по крайней мере при использовании «традиционного ООП».
          • –2
            Речь не об отдельных примерах, а об основной массе кода.
            О чем, кстати, я упомянул в своем предыдущем комментарии.
        • 0
          [смотрит на анонимные минусы]
          Бугога, похапешники такие похапешники 8)
  • +10
    Спасибо, интересно и познавательно!
  • +5
    Интересно, заприметил один новый момент для себя. Было бы неплохо написать еще одну статью по работе с объектами, чтобы было все в одном месте (на хабре), а не разбросано по интернету.

    Еще всегда задавался вопросом, почему подобные вещи не пишут в книжках аля «учебники по PHP для профессионалов»?! По крайней мере я держал в руках пару подобных книжек (одна из них была увесистой, страниц в 500) и не помню, чтобы там были затронуты подобные моменты оптимизации.
    • 0
      Ой, очень интересуюсь подобной литературой (аля «учебники по PHP для профессионалов»). Не подскажите, что за книги? Можно и не по PHP — C/C++, JAVA, Windows & Linux programming, Windows, Linux… тоже вполне подойдут — хоть сюда, хоть в личку.

      PS А как Вы «елочки» ставите, неужели через Alt или Copy&Paste? )
      • +1
        «Ёлочки» тут Хабр сам ставить умеет. А вообще их все обычно вводят при помощи кастомных раскладок, вроде бирманской, она и другие расхожие типографические символы позволяет проще вводить.
        • +8
          Ёлочки в Windows:
          « ALT (зажать) + 0171 (на цифровой клавиатуре)
          » ALT + 0187
          Аналогично можно и другие символы вставлять
          • 0
            Ещё можно запустить charmap в Windows или Gnome'овский gucharmap в Linux, и там выбрать нужные символы. Не помню, как там в виндовской утилите, но в линуксовой можно выбирать символы из Unicode-категорий типа «Mathematical operators» и т.п. Очень удобно, я считаю.
            • 0
              В виндовом charmap точно есть поиск по описанию символа.
          • –3
            Класс, теперь я знаю как вставлять дробь ½ ¼ и писать вот такие знаки ¿ █ “ ”
            В общем спасибо надо подучить.
        • +1
          Хм. Не посмотрев ссылку задался вопросом — неужели в Бирме настолько развиты компьютерные технологии.
        • 0
          Ёлочки в Mac OS:
          « — Alt + ъ
          » — Alt + Shift + ъ
    • +3
      >почему подобные вещи не пишут в книжках
      потому что не нужно? на мое имхо вот это все надо в 2% случаев, и то надо сильно подумать перед тем как лезть в эти дебри, а не просто поставить лимит побольше. php скрипт работает доли секунды, потом завершается. Даже в режиме FASTCGI где это ОЧЕНЬ сильно не нужно.

      а так, тут просто собрание коллекции фактов, без выводов.
      на мое имхо вся эта статья умещается в одну фразу «copy on write». Знание значения этой фразы заменяет всю эту статью.

      НЕ рассмотрен сборщик мусора в разных версиях, а это самое интересное.

      НЕ рассмотренно сколько памяти аллокированно скриптом и отдается ли она системе. а то у особо везучих получается веселая вещь — вроде mem usage показывает ноль, а скрипт отваливается по нехватке памяти.
      >Таким образом PHP действительно избавляет от необходимости использовать передачу по >ссылке для оптимизации использования памяти
      вот тут самое смешное то, что передача по ссылке если внутри функции переменная не меняется УХУДШАЕТ быстродействие

      >В остальном же мы видим, что если массив, используемый для цикла, имеет больше чем одну >ссылку на себя, тогда для него не применяется оптимизация из примера №1
      опять — а скорость померять? а количество zval'ов посмотреть?
      • +11
        Если у вас есть что сказать — напишите свою статью, в которой раскроете эти более интересные факты.
        И я считаю, что это, все же, нужно знать. Если вы думаете иначе — не стоит читать такие статьи :)
      • +4
        1. Насчет 2% случаев это очень условно. Если человек пишет только мини-скрипты «на коленке», тогда конечно да. Если же приложения по-серьезнее, то учитывать такие моменты придется уже не в 2% случаев, а например в каждом скрипте, где есть обработка данных из БД.
        Я сам был бы рад узнать всё это из книг, но к сожалению мне таковые не встречались пока.

        2. Не совсем понимаю каких именно выводов вы ожидали? Сколько ситуаций — столько и выводов. Здесь важен именно механизм, а применять ли его и как именно — это уже вопрос конкретной ситуации.

        3. Я не стремился впихнуть абсолютно всё в одну статью, да и сама статья называется «Работа с памятью», а не «Как PHP управляет памятью». Согласитесь, что это немного разные вещи. И хотя сборщик мусора и важная вещь, но если не считать редкие случаи использования коллектора циклических ссылок в 5.3, то вы практически никогда не будете использовать его сами напрямую в скрипте.

        4. Использование памяти скриптом, а именно аллокация при старте и последующей работе, в очень значительной мере зависит от самой OS, интерпретатора и их настроек. Это уже не только вопрос работы с памятью в скрипте, а гораздо более широкое понятие со множеством уникальных случаев. И данная тема также не является предметом данной статьи.

        5. Насчет ухудшения быстродействия хотелось бы увидеть от вас пример или ссылочку. Если я ничего не путаю, то передача по ссылке просто изменяет значения refcount__gc и is_ref__gc в _zval_struct. Не уверен что это как-то значительно скажется на быстродействии, если вообще скажется.

        6. Посмотрите в пример внимательнее. Там прекрасно видно когда выделяется новая память, а когда используются ссылки. Если вы считаете что это не говорит о количестве новых zval контейнеров и и переменных, тогда было бы замечательно увидеть от вас пример, что это не соответствует действительности и приводит к потере скорости. Полагаю что многим было бы интересно прочитать про найденный вами баг, поправлю статью при необходимости. Касательно скорости опять же не совсем понимаю где должна произойти ее потеря, если речь идет только об изменении счетчиков ссылок.

        В целом же по вашему комментарию можно сказать, что вы знаете что-то, что очень было интересно многим прочитать. Было бы интересно увидеть от вас статью на указанные вами темы.
        • 0
          Насчет ухудшения быстродействия хотелось бы увидеть от вас пример или ссылочку
          function a1(array $a){
          echo count($a);
          }
          function a2(array &$a){
          echo count($a);
          }

          $a = array(1,2,3,4,5,6,7,8,9,0)

          a1($a);
          a2($a);

          какая функция будет быстрее и почему?

          • +6
            Интересный у вас подход. Сами сделали утверждение и просите других, чтобы вас убедили и предоставили вам доказательства )
            Я уже написал вам: «Не уверен что это как-то значительно скажется на быстродействии, если вообще скажется.»

            И тем не менее сделаю вам одолжение:
            Оборачиваем ваш пример в код для отладки и проверки памяти.
            Смотрим xdebug_debug_zval('a'); внутри вызова функции:
            в случае с function a1(array &$a) получаем
            a: (refcount=3, is_ref=1)=array (… )
            в случае с function a2(array $a) получаем
            a: (refcount=3, is_ref=0)=array (… )

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

            Давайте даже посмотрим OpCode двух этих функций:
            Через копирование: www.heypasteit.com/clip/05DW
            Через ссылку: www.heypasteit.com/clip/05DX
            Разница между двумя опкодами только: в 7-ой строке SEND_VAR или SEND_REF

            Если у вас есть желание, можете копнуть еще глубже (тут уже только в С код) и найти всё же те различия, о которых говорите.
            • 0
              >php test.php 1 1000000
              524288 524288 335936 335936 1.672581911087

              >php test.php 2 1000000
              524288 524288 335936 335936 0.5970458984375
              • +1
                То есть, если я правильно понял этот набор цифр, вы предлагаете только ради экономии в 1 секунду на миллион вызовов функции count() везде применять передачу по значению?

                Кстати если вы уберете count() из функции, и скажем замените какой-либо операцией вроде $b = strval($a[0]);, то разница исчезнет, так что даже ваша секунда скорее всего просто связана с тем, как count работает со ссылками и значениями, а вовсе не с передачей ссылки или значения в функцию.
                • 0
                  >И тем не менее сделаю вам одолжение
                  >Насчет ухудшения быстродействия хотелось бы увидеть от вас пример или ссылочку.
                  спасибо не надо. вы бы хоть простейший тест прогнали, на этом коде.
                  тут дело в принципе. это простой кейс отвечающий на ваш пятый пункт
        • 0
          >Использование памяти скриптом, а именно аллокация при старте и последующей работе, в очень >значительной мере зависит от самой OS, интерпретатора и их настроек

          >И хотя сборщик мусора и важная вещь, но если не считать редкие случаи использования коллектора >циклических ссылок в 5.3
          вы не правы.

          выделяем 2/3 доступной памяти в переменную
          делаем ансет этой переменной
          выделяем 2/3 памяти в другую переменную
          отваливаемся с Allowed memory size exhauste
          bugs.php.net/bug.php?id=53031
          • 0
            Пожалуйста, внимательно читайте то, что написано в ссылках, которые вы даете. Первый же комментарий от администрации: [2010-10-09 18:02 UTC] johannes@php.net
            "… описанная вами ситуация не является багом. Пожалуйста, прочитайте документацию касательно того, как обрабатываются циклические ссылки de3.php.net/manual/en/features.gc.collecting-cycles.php ...".
            • 0
              я не говорил что это баг. я хотел сказать что не рассмотрена вот такая достаточно тривиальная ситуация, с фатальными последствиями, в которой без принудительного вызова сборщика мусора не обойтись.
              это относилось к процитированым фразам
              тут
              1) использование памяти НЕ зависит от ОС и настроек интерпритатора
              2) тут есть сборщик мусора и нет циклических ссылок.
              • 0
                и — это опровергает ваш ваш вывод:
                >Как видите, переменная не была полностью уничтожена. Под нее остается выделенным еще >76 байт.
                • 0
                  Может поясните, каким это образом это опровергает?
                  В вопросе программирования я как бы привык доверять больше цифрам, нежели умозрительным заключениям. Пока же цифры показывают, что память остается. И это абсолютно логично, так как переменная не уничтожается, а присваивается null значение (остается zval контейнер и запись об имени переменной).

                  Хотелось бы увидеть что-то конкретное, иначе это уже смахивает на троллинг )
                  • 0
                    о хосподи. в языках со сборщиком мусора память НЕ ОСВОБОЖДАЕТСЯ до вызова (руками или системой) этого самого сборщика.
                    ссылку на кейс я привел выше. вы же говорите что освобождается. вопрос подтверждаете ли вы этот кейс или нет, и если нет, то что надо сделать?
                    • +1
                      Прочитайте (еще раз) вот этот комментарий. Измените и еще раз проверьте ваш кейс.
                      И ответьте на первый вопрос комментария.
                      Полагаю это разрешит вопрос.

                      В PHP высвобождается память на основе подсчета ссылок. Если переменная выходит из области видимости, то у нее обнуляется счетчик ссылок и она удаляется. Тоже самое происходит и при unset. Память высвобождается сразу. Так что тут вы не правы. Стандартный сборщик мусора как раз и занимается таким обнулением и очисткой, при выходе из функций, завершении скрипта и т.д. Сборщик циклических ссылок занимается этим только при переваливании числа ссылок за 10000 или же при ручном вызове.
                      Если бы всё было идеально так как вы говорите, тогда бы налицо был виден баг в отображении информации потребления памяти через memory_get_usage(), и мы бы видели эти утечки, или «задержки обработки сборщика мусора».

                    • 0
                      Решил всё-таки найти в документации.
                      Почитайте: de3.php.net/manual/ru/features.gc.refcounting-basics.php
                      Особенно смотрим вот это:
                      «Как только „refcount“ станет равным нулю, контейнер уничтожается. „refcount“ уменьшается на единицу при уходе переменной из области видимости (например, в конце функции) или при вызове unset() с данной переменной.
                      Если мы сейчас вызовем unset($a);, то контейнер, включая тип и значение, будет удален из памяти.»
                      Ну или мы налицо имеем баг документации PHP.
                      Если я что-то упустил и что-то не так понимаю — подскажите где почитать.
                      • 0
                        bugs.php.net/bug.php?id=53031
                        здесь. подтверждаете ли вы этот кейс или нет?
                        • 0
                          Конечно нет.
                          Потому что в постер этого «бага» явно не читает документацию по сбору циклических ссылок. Об этом прямо ответила администрация.
                      • 0
                        Как только „refcount“ станет равным нулю, контейнер уничтожается. „refcount“ уменьшается на единицу при уходе переменной из области видимости (например, в конце функции) или при вызове unset() с данной переменной.

                        плохо читали:
                        To avoid having to call the checking of garbage cycles with every possible decrease of a refcount…
                        Only when the root buffer is full does the collection mechanism start for all the different zvals inside. See step A in the figure above.
                        • 0
                          Во первых, указывайте откуда вы берете цитаты.
                          В данном случае это цитата отсюда: php.net/manual/en/features.gc.collecting-cycles.php
                          Это касается описания сборщика мусора для ЦИКЛИЧЕСКИХ ССЫЛОК, который появился в 5.3, а не обычного сборщика мусора PHP, про который я писал вам.

                          Это два разных сборщика мусора.

                          Да вашей цитате есть очень важное слово: Only when the root buffer is full does the collection mechanism start for all the different zvals inside.

                          Сборщик циклических ссылок запускается когда заполняет буфер «корневых ссылок».

                          Вы начинаете путать одно с другим.
      • НЛО прилетело и опубликовало эту надпись здесь
        • –3
          Тема демонов несомненно имеет место быть.

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

          • НЛО прилетело и опубликовало эту надпись здесь
            • 0
              Абсолютно согласен.

              Но подход о котором я упомянул, это не решение проблем с утечкой памяти (хотя и помогает), а скорее вариант обхода ограничений, особенно на время исполнения скрипта.
              • НЛО прилетело и опубликовало эту надпись здесь
    • –1
      Может быть потому что профессионалы не читают книг «для профессионалов»?
  • –1
    всегда интересовало, почему люди пишут

    echo «sutff». PHP_EOL

    если можно без конкатенаций (,)?
    • +3
      автоматом чтобы не думать где используется в echo или print или wrapper($str) короче просто привыкли люди что строки сшиваются точкой. пытался однажды с этим бороться — бесполезно.
      • 0
        Вопрос немного в офтоп:
        Где можно почитать про оператор «запятая» в PHP? Чисто с т.з. синтаксиса, какое у него поведение.
        • 0
          Такое же как и остальных местах — перечисление параметров.
          Просто именно у echo есть особенность поведения:
          echo 'hello ', 'world!';
          равносильно:
          echo('hello ');
          echo('world!');

          Это кстати одно из ее отличий от print, который не позволяет такую передачу параметров.
          • 0
            Ну судя по всему, еще один «костыль» в грамматике.
            Интересно, что в мануале запятая заявлена как оператор (в разделе с приоритетами операций), между тем она может быть только частью определенных конструкций.
            • 0
              А к чему бы Вы отнесли запятую?
              • 0
                Отвечу вопросом на вопрос — А к чему бы вы отнесли скобку?

                При таком раскладе — просто лексема, для которой существует узкий контекст применимости.
                Вопрос мой возник из-за того, что я считал php в большей степени C-style языком. К слову, в Си (и в плюсах, соответственно тоже) запятая — это оператор, возвращающий значение справа.
                В JS (можете проверить) это тоже справедливо:
                var x = (12, 51); 
                alert(x);
                выведет 51. Запятая — это часть expression со своим приоритетом и поведением.

                Если говорить про php, то смысл приоритета запятой для меня неясен. Либо документацию писал некомпетентный человек, либо сам язык не имеет дизайна (что объясняет по крайней мере отсутствие официальных спецификаций).
                Как кто-то однажды сказал, «Простота — результат глубокого анализа, сложность — результат нагромождения».
                • 0
                  С дизайном у PHP действительно не очень. Начинал он свой путь с простого шаблонизатора, стараясь сохранять обратную совместимость. Единой концепции, формального описания синтаксиса, да даже соглашений об именовании функций и т. п. нет и вроде бы не предвится.
    • +14
      Потом легко поменять на:
      $a = «stuff». PHP_EOL;

      А вам придется все запятые заменять.
  • +3
    Отличный пост! Не знал многие моменты, хотя уже не год программирую на php.
    • +1
      Зашли в статью украдкой поностальгировать?
      • +13
        И почему я прочитал «хотя уже год не программирую...»?
        • +1
          Не вы один.
        • +1
          Может быть это бессознательное желание на годик забить на PHP и отправиться работать горнолыжным инструктором в альпийский пансион для благородных девиц? ))
  • +1
    Спасибо за статью. Отметьте только, что фишка с циклом и массивами касается только foreach цикла.
  • 0
    для наглядности не хватает примеров с опкодами
    а так статья замечательная
  • +1
    Статья превосходна, спасибо.
    Правда я не совсем понимаю, зачем в начале кода два раза подряд использовать $base_memory_usage = memory_get_usage();
    • +1
      Присвоение значения переменной $base_memory_usage добавит лишних 76 байт.
    • +3
      Для удобства просмотра расхода памяти. Первое присвоение создает переменную и выделяет под нее память, но эта память «до» присвоения, второе же присвоение уже содержит объем памяти с учетом переменной $base_memory_usage.
      • 0
        Теперь понял, спасибо :)
      • +3
        А если сделать
        $base_memory_usage = 0; // memory allocation
        $base_memory_usage = memory_get_usage();

        Просто как-то действительно не очевидно, как будто опечатка выглядит.
        • +2
          Нет предела совершенству.
          Но если бы всё было так очевидно, не приходилось бы задумываться, спрашивать и узнавать, а в этом есть своя польза ;)
  • +3
    > Получается, что в последней итерации цикла foreach в данном случае потребление массивом памяти возросло в два раза, хотя по самому коду это не очевидно. Но сразу после цикла, потребление памяти вернулось к прежнему значению. Чудеса да и только.
    > Причиной тому является оптимизация использования массива в цикле. На время работы цикла, при попытке изменить исходный массив, неявно создается «массив изменений исходного массива» (не копия), который «применяется» к исходному массиву сразу по завершению цикла.

    Здесь написана какая-то хрень.
    foreach($a as $k => $v) исходный массив копирует полностью при первом изменении (то же самое происходит при любой передаче «по значению» в функцию).
    Чтобы этого не было, надо писать foreach($a as $k => &$v)

    Но! Копирование массива в php не приводит к deep copy всех его значений, копируется (и дальше изменяется) только его хэш таблица.
    Получаем два независимых массива, у которых, тем не менее, память под одинаковые элементы выделяется один раз.
    Поэтому overhead от копирования зависит от того, что считать «большим» массивом.
    Если это массив из пяти элементов пусть даже по мегабайту каждый, копирование будет почти бесплатным.
    Если же это массив вида range(0, 100500), то есть из большого числа мелких элементов, то его копия сожрет куда больше памяти.

    proof:
    <?php
    
    echo memory_get_usage() . "\n";
    $a = range(0, 100500);
    echo memory_get_usage() . "\n";
    
    foreach ($a as $k => $v)
    {
    	if ($k == 0 || $k == 1 || $k == 100500) echo memory_get_usage() . "\n";
    	$a[$k] = 1;
    }
    


    Видно, что на первой итерации цикла копий вообще никаких нет (пока массив мы не меняем), на второй итерации создается полная копия массива $a (foreach продолжает бегать по старой копии) и это съедает сразу много памяти, дальше до конца память постепенно выделяется на zval-ы отдельных заменяемых элементов.
    Если заменить последнюю строку на $a[$k] = $a[$k], увидим, что выделения памяти на новые zval больше не будет, но копия массива один раз все равно создастся.

    Еще раз размещу ссылку на вот эту замечательную статью, там эти вопросы довольно неплохо разжеваны.
    • +1
      Здесь написана какая-то хрень.
      foreach($a as $k => $v) исходный массив копирует полностью при первом изменении (то же самое происходит при любой передаче «по значению» в функцию).

      Полного копирования не происходит. Посмотрите внимательнее на примеры в статье. Если под «полностью» мы подразумеваем с вами и структуру массива и его значения. Либо мы с вами друг друга не поняли, и тогда прошу вас привести конкретный пример, чтобы было понятнее. Мне интересны ваши доводы.

      В остальном ваши замечания справедливы. Не описал в статье явно эту тонкость работы, а именно различие между копированием структуры и копированием значений, что становится существенно при большом числе элементов, а не объеме значений.

      С вашего позволения добавлю ссылку в статью и на ваш комментарий, чтобы информация получилась более целостной.
      • +1
        Довод всего лишь в том, что никаких «массивов изменений» в природе не существует, есть две независимые друг от друга копии структуры со ссылками на одни и те же значения.
        Ничего по завершению цикла никуда само не применяется, тот массив, по которому бежит foreach, и переменная, доступная по имени $a — это две разные переменные, и первая из них по завершению цикла просто выбрасывается.
        Ссылку можете добавить, но и по тем ссылкам, что уже приведены, написано примерно то же самое :)
        • +1
          Принимаю ваши доводы, пусть суть и не меняется, но термин выбран не корректно и может запутать тех кто столкнется с этим явлением впервые. Поправил статью. Спасибо за уточнения.
  • +1
    Следить за памятью нужно при использовании расширений — PDFLib может запросто отожрать гигабайт.
  • +4
    Скорее всего потерянные 88 байт – это ещё одна «магическая» оптимизация php.

    При создании объекта, если в нём не было изменений («объект не понадобился») он затем используется, при создания объекта повторно. Звучит запутано, но вот пример:
    $a = new stdClass();
    echo spl_object_hash($a); //000000007843f529000000002ad1ffeb
    unset($a);
    echo spl_object_hash(new stdClass()); //000000007843f529000000002ad1ffeb


    Тоже самое без unset вернёт разные хеши объектов.

    При этом если использовать какой-то класс имеющий конструктор – объект не будет создан повторно (в хе таблице), но будет вызван повторно конструктор.

    Это будет иметь высасаный из пальца сайд эффект в случае если вам зачем-то нужно хеши созданных объектов где-нибудь хранить (зачем я не смог придумать):
    <?php
    class A {
    public function __construct() {
    echo ' [construct] ';
    AllA::addHash(spl_object_hash($this));
    }
    }

    class AllA {
    static protected $_hashes = array();
    static public function addHash($hash) {
    self::$_hashes[] = $hash;
    }

    static public function getHashes() {
    return self::$_hashes;
    }
    }

    echo 'new A: '; $a = new A();
    echo 'a hash: ', spl_object_hash($a), PHP_EOL; //new A: [construct] a hash: 000000001b77b1850000000057548e86
    echo 'unset a', PHP_EOL;
    unset($a);
    echo 'object with no referance hash: ',
    spl_object_hash(new A()), PHP_EOL;
    //object with no referance hash: [construct] 000000001b77b1850000000057548e86

    echo 'All hashes:', PHP_EOL, join(PHP_EOL, AllA::getHashes());
    //здесь будут два одинаковых значения 000000001b77b1850000000057548e86

    • 0
      Уточню, что созданный «чистый» объект будет жить до конца выполнения скрипта и будет всегда использоваться для создания новых объектов. Однако при хотябы каком-нибудь изменении будет создаваться копия, куда уже эти изменения и попадут.

      P.S Что-то по воскресеньям не могу излагать мысли в простом виде: )
      • +1
        Боюсь, что spl_object_hash возвращает просто хэш от идентификатора объекта, который можно посмотреть var_dump.

        class asf{
            public $asf = 1;
        }
        $a = new asf();
        var_dump($a);
        echo spl_object_hash($a).PHP_EOL; //00000000702b718*8*000000003d3a961b
        var_dump($a); //object(asf)#*1* (1) {["asf"]=>int(1)}
        unset($a);
        $b = new stdClass();
        echo spl_object_hash($b).PHP_EOL; //00000000702b718*8*000000003d3a961b
        var_dump($b); //object(stdClass)#*1* (0) {}
        $c = new asf();
        echo spl_object_hash(new asf()).PHP_EOL; //00000000702b718*a*000000003d3a961b
        var_dump($c); //object(asf)#*2* (1) {["asf"]=>int(1)}
        

        Разница в идентификаторах и хэшах выделена "*".
        • 0
          Именно так он и поступает :) За подробностями можно заглянуть в ext/spl/php_spl.c (и поискать php_spl_object_hash), причём базовый хеш намерено рандомизируется в кажом вызове.

          Я таким образом хотел проиллюстрировать, что это один и тот же объект. Можно это понять и через var_dump, но как-то менее наглядно, плюс мне нужно было значение в виде переменной.
          • +1
            Дык, ни о каком прототипировании речи быть не может. Посмотрите в моём коде разные объекты разных классов имеют одинаковый id и одинаковый хэш, а объекты одного и того же класса имеют разные id и разные хэши.

            class Foo{
                protected $_test = 100;
            }
            
            $foo1 = new Foo();
            $foo2 = new Foo();
            $foo3 = new Foo();
            
            echo spl_object_hash($foo1).PHP_EOL; //000000005e3115f60000000047e5edc1
            var_dump($foo1); // object(Foo)#1 (1) {["_test":protected]=>int(100)}
            echo spl_object_hash($foo2).PHP_EOL; //000000005e3115f50000000047e5edc1
            var_dump($foo2); // object(Foo)#2 (1) {["_test":protected]=>int(100)}
            echo spl_object_hash($foo3).PHP_EOL; //000000005e3115f40000000047e5edc1
            var_dump($foo3); //object(Foo)#3 (1) {["_test":protected]=>int(100)}
            
            $foo1 = new stdClass();
            echo spl_object_hash($foo1).PHP_EOL; //000000005e3115f30000000047e5edc1
            var_dump($foo1); //object(stdClass)#4 (0) {}
            
            $foo1 = new stdClass();
            echo spl_object_hash($foo1).PHP_EOL; //000000005e3115f60000000047e5edc1
            var_dump($foo1); //object(stdClass)#1 (0) {}
            

            Или вот такой пример, который показывает, что id объекта не всегда увеличивается, в качестве id берётся первое свободное значение начиная с 1.
  • +2
    Странные эти 88 байт.
    memoryUsage(memory_get_usage(), $base_memory_usage);//Bytes diff: 0 
    memoryUsage(memory_get_usage(), $base_memory_usage);//Bytes diff: 88 
    


    P.S. То, что выводит скрипт пишите сразу в коде в конце строки — так смотреть гораздо удобнее и статья получается короче.
  • +2
    Спасибо, давненько ничего такого полезного не было!
  • –2
    > Но сразу после цикла, потребление памяти вернулось к прежнему значению

    Каждый foreach делает копию массива
  • 0
    А меня вот еще какой вопрос мучит:
    PHP интерпретатор. Значит он в процессе выполнения где-то имена переменных хранит. Конечно эта память после освобождается, но пока скрипт отсчитывается — где-то все хранится. А раз хранится, то длинна значения переменных что-то значит. Код с использованием переменной iNomberOfMonkeysOnThePalmThee будет больше кушать чем с переменной iMonkey///Возможно это копейки, но тем не менее.

    Возможно я чайник-кофейник и глобально ошибаюсь. Просто у меня большой опыт на С++, а все мои потуги с PHP выполняются где-то на дальных серверах и как их померить вообще загадка. Это вам не ЦЦ-профайлер, где можно было по байтам код расковыривать…
    • +1
      Длина переменных в PHP не является как таковой проблемой, потому как еще на стадии OpCoda происходит определенная оптимизация. Это можно видеть по например такому опкоду ( www.heypasteit.com/clip/05DW ):
      compiled vars: !0 = $a // тут в !0 используется как идентификатор для переменной с именем $a
      // и дальше работа уже непосредственно с этим идентификатором
      INIT_ARRAY ~0 // Создаем массив ~0
      ASSIGN !0, ~0 // Присваиваем $a созданный массив

      Так что имена переменных конечно хранятся, но жертвовать их длиной в ущерб читаемости — это «экономия на спичках».
      • 0
        Спасибо…
  • 0
    Познавательно, спасибо за статью!
  • 0
    Было бы очень мега-круто для полной картины воспользоваться debug версией php и продебажить выполнение. Так как всё описанное лишь косвенное, хотя и показательно.

    ПС. Я б и сам сделал, но работы много сильно. Может через пол годика по тестирую, эту особенность надо запомнить.
  • НЛО прилетело и опубликовало эту надпись здесь

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