Русский
Русский
English
Статистика
Реклама

Перевод Dont Fear the Reaper

D, как и многие активно используемые сегодня языки, поставляется со сборщиком мусора (Garbage Collector, GC). Многие виды ПО можно разрабатывать, вообще не задумываясь о GC, в полной мере пользуясь его преимуществами. Однако у GC есть свои изъяны, и в некоторых сценариях сборка мусора нежелательна. Для таких случаев язык позволяет временно отключить сборщик мусора или даже совсем обойтись без него.


Чтобы получить максимальное преимущество от сборщика мусора и свести недостатки к минимуму, необходимо хорошо понимать, как работает GC в языке D. Хорошим началом будет страничка Garbage Collection на dlang.org, которая подводит обоснование под GC в языке D и даёт несколько советов о том, как с ним работать. Это первая из серии статей, которая призвана более подробно осветить тему.


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


Самая первая вещь, которую нужно уяснить: сборщик мусора в D запускается только во время выделения памяти и только в том случае, если нет памяти, которую можно выделить. Он не сидит в фоне, периодически сканируя кучу и собирая мусор. Это необходимо понимать, чтобы писать код, эффективно использующий память под контролем GC. Рассмотрим следующий пример:


void main() {    int[] ints;    foreach(i; 0..100) {        ints ~= i;    }}

Эта программа создаёт динамический массив значений типа int, а затем при помощи имеющегося в D оператора присоединения добавляет в него числа от 0 до 99 в цикле foreach. Что неочевидно неопытному глазу, так это то, что оператор присоединения выделяет память для добавляемых значений через сборщик мусора.


Реализация динамического массива в рантайме D вовсе не тупая. В нашем примере не произойдёт сотни выделений памяти, по одному на каждое значение. Когда требуется больше памяти, массив выделяет больше памяти, чем запрашивается. Мы можем определить, сколько на самом деле будет выделений памяти, задействовав свойство capacity. Это свойство возвращает количество элементов, которые можно поместить в массив, прежде чем потребуется выделение памяти.


void main() {    import std.stdio : writefln;    int[] ints;    size_t before, after;    foreach(i; 0..100) {        before = ints.capacity;        ints ~= i;        after = ints.capacity;        if(before != after) {            writefln("Before: %s After: %s",                before, after);        }    }}

Скомпилировав этот код в DMD 2.073.2 и выполнив, мы увидим шесть сообщений шесть выделений памяти через GC. Это означает, что у GC шесть раз была возможность собрать мусор. В этом маленьком примере это вряд ли произошло. Если бы этот цикл был частью большой программы, использующей GC, то это вполне могло бы произойти.


Кроме того, любопытно взглянуть на значения before и after. Программа выдаёт последовательность: 0, 3, 7, 15, 31, 63, и 127. После выполнения цикла массив ints содержит 100 значений, и в нём есть место под ещё 27 значений, прежде чем произойдёт следующее выделение памяти, которое увеличит объём массива до 255, экстраполируя предыдущие значения. Это, однако, уже детали реализации рантайма D, и в будущих релизах всё может поменяться. Чтобы узнать больше о том, как GC контролирует массивы и срезы, взгляните на прекрасную статью Стива Швайхоффера (Steve Schveighoffer) на эту тему.


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


Даже когда речь идёт о языках без встроенного сборщика мусора, таких как C и C++, большинство программистов рано или поздно узнают, что для общей производительности лучше заранее выделить как можно больше ресурсов и свести к минимуму выделение памяти во внутренних циклах. Это одна из многих преждевременных оптимизаций, которые не является корнем всех зол то, что мы называем лучшими практиками. Учитывая, что GC в языке D запускается только когда происходит выделение памяти, ту же самую стратегию можно применять как простой способ минимизировать его влияние на производительность. Вот как можно переписать пример:


void main() {    int[] ints = new int[](100);    foreach(i; 0..100) {        ints[i] = i;    }}

Мы сократили шесть выделений памяти до одного. Единственная возможность для GC запуститься перед внутренним циклом. Этот код выделяет место для по крайней мере 100 элементов и инициализирует их значением нулями перед входом в цикл. После new длина массива будет 100, но в нём почти наверняка будет дополнительная ёмкость.


Есть также другой способ: функция reserve:


void main() {    int[] ints;    ints.reserve(100);    foreach(i; 0..100) {        ints ~= i;    }}

Это выделит память под по крайней мере 100 значений, но массив всё ещё будет пустым (его свойство length будет возвращать 0), так что ничего не будет инициализировано значениями по умолчанию. Учитывая, что цикл добавляет только 100 значений, гарантируется, что выделения памяти не произойдёт.


Помимо new и reserve, можно также выделять память явным образом, напрямую вызывая GC.malloc.


import core.memory;void* intsPtr = GC.malloc(int.sizeof * 100);auto ints = (cast(int*)intsPtr)[0 .. 100];

Литералы массивов обычно выделяет память.


auto ints = [0, 1, 2];

Это верно также в том случае, когда литерал массива используется в enum.


enum intsLiteral = [0, 1, 2];auto ints1 = intsLiteral;auto ints2 = intsLiteral;

Значение типа enum существует только во время компиляции и не имеет адреса в памяти. Его имя синоним его значения. Где бы вы его не использовали, это будет как если бы вы скопировали и вставили его значение на месте его имени. И inst1, и inst2 вызовут выделение памяти, как если бы мы определили их вот так:


auto ints1 = [0, 1, 2];auto ints2 = [0, 1, 2];

Литералы массивов не выделяют память, если целью является статический массив. Кроме того, строковые литералы (строки в D это массивы) исключение из общего правила и также не выделяют память.


int[3] noAlloc1 = [0, 1, 2];auto noAlloc2 = "No Allocation!";

Оператор конкатенации всегда выделяет память:


auto a1 = [0, 1, 2];auto a2 = [3, 4, 5];auto a3 = a1 ~ a2;

У ассоциативных массивов в D своя стратегия выделения памяти, но можно ожидать, что они будут выделять память при добавлении элементов и при их удалении. Также они реализуют два свойства: keys и values, которые выделяют память под массив и заполняют его копиями соответствующих элементов. Когда ассоциативный массив нужно изменять во время того, как вы по нему итерируете, или если нужно отсортировать элементы или ещё каким-то образом обработать их в отрыве от массива, эти свойства то что доктор прописал. В других случаях это лишь лишнее выделение памяти, чрезмерно нагружающее GC.


Когда сборка мусора всё-таки запускается, время, которое она займёт, будет зависеть от объёма сканируемой памяти. Чем меньше, тем лучше. Никогда не будет лишним избегать ненужных выделений памяти, и это ещё один хороший способ минимизировать влияние сборщика мусора на производительность. Как раз для этого у ассоциативных массивов в D есть три свойства: byKey, byValue и byKeyValue. Каждое из них возвращает диапазон, который можно итерировать ленивым образом. Они не выделяют память, поскольку напрямую обращаются к элементам массива, поэтому не следует его изменять во время итерирования. Более подробно о диапазонах можно прочитать в главах Ranges и More Range из книги Али Чехрели (Ali ehreli) Programming in D.


Замыкания делегаты или функции, которые должны нести в себе указатель на фрейм стека также выделяют память. Последняя возможность языка, упомянутая на страничке Garbage Collection выражение assert. Если проверка проваливается, выражение assert выделяет память, чтобы породить AssertError, которое является частью иерархии исключений языка D, основанной на классах (в будущих статьях мы рассмотрим, как классы взаимодействуют с GC).


И наконец, есть Phobos стандартная библиотека D. Когда-то давным-давно большая часть Phobosа была реализована без особой заботы о выделении памяти через GC, отчего его трудно было использовать в ситуациях, когда это было нежелательно. Однако потом было приложено много усилий, чтобы сделать его более сдержанным в обращении с GC. Некоторые функции были переделаны, чтобы они могли работать с ленивыми диапазонами, другие были переписаны, чтобы они принимали буфер, а некоторые были переработаны, чтобы избежать лишних выделений памяти внутри себя. В результате стандартная библиотека стала гораздо более пригодной для написания кода, свободного от GC (хотя, возможно, ещё остаются места, которые ещё предстоит обновить пулл-реквесты всегда приветствуются).


Теперь, когда мы разобрались с основами работы с GC, в следующей статье в этой серии мы рассмотрим инструменты, предоставляемые язык и компилятор, которые позволяют отключать GC и гарантировать, что критические места программы свободны от GC.


Спасибо Гийому Пьола (Guillaume Piolat) и Стиву Швайхофферу за их помощь в подготовке этой статьи.

Источник: habr.com
К списку статей
Опубликовано: 03.07.2020 00:15:20
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

D

Высокая производительность

Dlang

Gc

Garbage collector

Категории

Последние комментарии

© 2006-2020, personeltest.ru