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

Перевод Go Your Own Way. Часть первая. Стек

Серия статей о GC
  1. Dont Fear the Reaper
  2. Life in the Fast Lane
  3. Go Your Own Way. Часть первая. Стек
  4. Go Your Own Way. Часть вторая. Куча

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


Когда сборщик мусора отключён через GC.disable или запрещён к использованию атрибутом функции @nogc, память всё ещё надо откуда-то выделять. И даже если вы используете GC на всю катушку, всё равно желательно минимизировать объём и количество выделений памяти через GC. Это означает выделение памяти или на стеке, или в обычной куче. Эта статья будет посвящена первому. Выделение памяти в куче будет предметом следующей статьи.


Выделение памяти на стеке


Самая простая стратегия выделения памяти в D такая же, как в C: избегать использование кучи и использовать стек, насколько возможно. Если нужен массив, и его размер известен во время компиляции, используйте статический массив вместо динамического. Структуры имеют семантику значений и по умолчанию создаются на стеке, а классы имеют семантику ссылок и обычно создаются в куче (обычной, в стиле C, или управляемой GC); предпочтение следует отдавать структурам, когда это возможно. Возможности D, доступные во время компиляции, помогают достичь многого из того, что иначе было бы невозможно.


Статические массивы


Статические массивы в D требуют, чтобы размер был известен во время компиляции.


// OKint[10] nums;// Ошибка: переменную x нельзя прочитать во время компиляцииint x = 10;int[x] err;

В отличие от динамических массивов, инициализация статических массивов через литерал не выделяет память через GC. Длины массива и литерала должны совпадать, иначе компилятор выдаст ошибку.


@nogc void main() {    int[3] nums = [1, 2, 3];}

Если передать статический массив в функцию, принимающую срез массива, то автоматически происходит получение среза, что делает статические массивы взаимозаменяемыми с динамическими.


void printNums(int[] nums) {    import std.stdio : writeln;    writeln(nums);}void main() {    int[]  dnums = [0, 1, 2];    int[3] snums = [0, 1, 2];    printNums(dnums);    printNums(snums);}

Находить при помощи флага -vgc места, где создаётся динамические массивы, и по возможности заменять их на статические простая и эффективная техника. Только берегитесь случаев вроде такого:


int[] foo() {    auto nums = [0, 1, 2];    // Сделать что-то с nums...    return nums;}

В этом примере заменить nums на статический массив было бы неправильно. Функция вернула бы срез памяти, выделенной на стеке, а это программная ошибка. К счастью, компилятор не даст вам этого сделать.


С другой стороны, если возвращаемое значение зависит от какого-то условия, имеет смысл выделить память в куче только при необходимости, а не при каждом вызове функции. В данном примере мы локально создаём статический массив, а на выходе из функции создаём его динамическую копию. Встречайте свойство .dup:


int[] foo() {    int[3] nums = [0, 1, 2];    // Пусть x  результат какой-то операции над nums    bool condition = x;    if(condition) return nums.dup;    else return [];}

Эта функция всё ещё выделяет память GC через .dup, но делает это только если это нужно. Обратите внимание, что в данном случае [] эквивалентен null это срез, у которого длина (свойство length) равна 0, а свойство ptr возвращает null.


Структуры против классов


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


struct Foo {    int x;    ~this() {        import std.stdio;        writefln("#%s says bye!", x);    }}void main() {    Foo f1 = Foo(1);    Foo f2 = Foo(2);    Foo f3 = Foo(3);}

Программа печатает то, что вы и ожидаете:


#3 says bye!#2 says bye!#1 says bye!

Классы, будучи типом с семантикой ссылок, почти всегда создаются в куче. Обычно это делается через GC при помощи new, хотя это можно сделать и без GC при помощи собственного аллокатора. Но никто не говорил, что их нельзя создать на стеке. При помощи шаблона [std.typecons.scoped](http://personeltest.ru/aways/dlang.org/phobos/std_typecons.html#.scoped) из стандартной библиотеки это очень просто сделать.


class Foo {    int x;    this(int x) {         this.x = x;     }    ~this() {        import std.stdio;        writefln("#%s says bye!", x);    }}void main() {    import std.typecons : scoped;    auto f1 = scoped!Foo(1);    auto f2 = scoped!Foo(2);    auto f3 = scoped!Foo(3);}

Этот пример функционально работает точно так же, как и предыдущий пример со структурами, и выводит тот же результат. Предсказуемое уничтожение достигается при помощи функции core.object.destroy, которая позволяет вызывать деструкторы вне сборок мусора.


Обратите внимание, что на данный момент ни scoped, ни destroy нельзя использовать в @nogc-функциях. Это не всегда плохо, потому что необязательно помечать функцию соответствующим аттрибутом, чтобы избежать GC, но это может стать головной болью, если вы пытаетесь запихнуть всё в @nogc-код. В следующих статьях мы рассмотрим некоторые проблемы проектирования, которые возникают при использовании nogc, и как их избежать.


При создании собственного типа выбор между структурой и классом в основном зависит от того, как его планируется использовать. Для простых данных (Plain Old Data, POD) очевидным кандидатом будет структура, тогда как для какого-нибудь GUI, где крайне полезны будут иерархии наследования и динамические интерфейсы, предпочтительным будет класс. Кроме этих очевидных случаев, есть также ряд соображений, которые могут послужить темой отдельного поста. Пока что держите в уме, что вне зависимости от того, используете ли вы для своего типа структуры или классы, его экземпляры всегда можно создавать на стеке.


alloca


Поскольку стандартная библиотека C доступна в языке D из коробки, для выделения памяти на стеке также можно использовать функцию alloca. Она особенно полезна, когда для массива желательно избежать использования GC, но размер массива неизвестен во время компиляции. Следующий пример создаёт на стеке динамический массив, размер которого известен только во время выполнения:


import core.stdc.stdlib : alloca;void main() {    size_t size = 10;    void* mem = alloca(size);    // Slice the memory block    int[] arr = cast(int[])mem[0 .. size];}

Как и в C, при использовании alloca нужно соблюдать осторожность: не переполните стек. И как и в случае с локальными статическими массивами, нельзя возвращать срез arr из функции. Вместо этого возвращайте arr.dup.


Простой пример


Предположим, что вы хотите создать тип Queue, представляющий структуру данных типа очередь. Характерной для D реализацией такого типа будет шаблонная структура, статическим параметром которой будет хранимый тип. В Java коллекции сильно опираются на интерфейсы, и рекомендуется объявлять экземпляр, используя тип интерфейса вместо типа имплементации. В D структуры не поддерживают наследование, но во многих случаях они могут реализовывать абстрактные интерфейсы благодаря проектированию через интроспекцию (Design by Introspection). Эта парадигма позволяет программировать общие интерфейсы, которые верифицируются во время компиляции без нужды в интерфейсе как отдельном типе, и потому могут работать со структурами, классами и, благодаря UFCS, даже свободными функциями (если они находятся в той же области видимости).


На русском про DbI можно прочитать на Хабре. (прим. пер.)

Очевидным выбором для внутреннего хранилища в нашей реализации Queue будут массивы. Кроме того, есть возможность сделать хранилище статическим массивом, если предполагается ограничить очередь фиксированным размером. Поскольку это уже и так шаблон, мы без труда добавим ещё один параметр: статический параметр-значение со значением по умолчанию. Теперь можно с лёгкостью указать во время компиляции, должен ли массив быть статическим, и если да, то сколько места он будет требовать.


// Размер `Size` по умолчанию 0 означает использование в качестве // внутреннего хранилища динамического массива; ненулевой размер// означает использование статического массиваstruct Queue(T, size_t Size = 0) {    // Тип константы будет автоматически определён как двоичный.    // Уровень доступа `public` позволит DbI-шаблону вне этого модуля    // определять, может ли Queue расти или нет.    enum isFixedSize = Size > 0;    void enqueue(T item)     {        static if(isFixedSize) {            assert(_itemCount < _items.length);        }        else {            ensureCapacity();        }        push(item);    }    T dequeue() {        assert(_itemCount != 0);        static if(isFixedSize) {            return pop();        }        else {            auto ret = pop();            ensurePacked();            return ret;        }    }    // Доступно только в очереди с неограниченным размером    static if(!isFixedSize) {        void reserve(size_t capacity) {             /* Выделить память под несколько элементов */         }    }private:       static if(isFixedSize) {        T[Size] _items;         }    else T[] _items;    size_t _head, _tail;    size_t _itemCount;    void push(T item) {         /* Добавить item, обновить _head и _tail */        static if(isFixedSize) { ... }        else { ... }    }    T pop() {         /* Изъять item, обновить _head и _tail */         static if(isFixedSize) { ... }        else { ... }    }    // Доступно только в очереди с неограниченным размером    static if(!isFixedSize) {        void ensureCapacity() { /* Выделить память, если нужно */ }        void ensurePacked() { /* Сжать массив, если нужно */}    }}

Теперь в клиентском коде можно вот так создавать экземпляры:


Queue!Foo qUnbounded;Queue!(Foo, 128) qBounded;

Очередь qBounded не выделяет память из кучи. Как работает qUnbounded, зависит от реализации. При помощи интроспекции можно проверить во время компиляции, имеет ли экземпляр очереди фиксированный размер. Для этого мы добавили удобную константу isFixedSize:


void doSomethingWithQueueInterface(T)(T queue){    static if(T.isFixedSize) { ... }    else { ... }}

Вместо этого можно было бы использовать встроенные возможности языка: __traits(hasMember, T, "reserve"), или стандартную библиотеку: hasMember!T("reserve"). Выражение __traits и пакет стандартной библиотеки std.traits отличные инструменты для DbI; последнему стоит отдавать предпочтение при схожей функциональности.


Заключение


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


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

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

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

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

D

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

Dlang

Garbage collector

Gc

Design by introspestion

Категории

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

© 2006-2020, personeltest.ru