Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Элджер Д. - C++. Библиотека программиста - 1999.pdf
Скачиваний:
141
Добавлен:
13.08.2013
Размер:
1.98 Mб
Скачать

Основы

14

 

управления

 

памятью

 

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

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

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

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

Строительные блоки

В числе основных принципов нестандартного управления памятью в С++ должен быть следующий: «Придумайте откровенную глупость; вполне возможно, из этого что-нибудь получится». Даже если идея не сработает сама по себе, она может пригодиться в качестве отправной точки.

Поблочное освобождение памяти

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

Фиктивное удаление

Задача многих программ — побыстрее отработать свое и уйти. Это особенно справедливо в среде Unix, где сценарии оболочки объединяют множество крошечных, недолговечных программ. Нередко выделение памяти для новых объектов оказывается самым серьезным фактором, снижающим быстродействие таких программ. Простая стратегия оптимизации заключается в том, что вы выделяете память под объекты снизу вверх большого блока и не удаляете их.

struct Pool {

 

static Pool* gCurrentPool;

// Пул для выделения памяти

enum {

block_size = 8096 };

// Выберите свой любимый размер

unsigned char* space;

// Следующая выделяемая область

size_t

remaining;

// Количество оставшихся байт в блоке

206

Pool() : space((unsigned char*)calloc(block_size, ‘\0’)), remaining(block_size) {}

void* Allocate(size_t bytes)

{

if (bytes > block_size)

 

return ::operator new(bytes);

// Слишком большой запрос

if (gCurrentPool == NULL || bytes ? remaining) gCurrentPool = new Pool;

void* memory = space; space += bytes; remaining -= bytes; return memory;

}

};

class Foo { public:

void* operator new(size_t bytes)

{

if (Pool::fCurrentPool == NULL) Pool::gCurrentPool = new Pool;

return Pool::gCurrentPool->Allocate(bytes);

}

void operator delete(void*) {}

};

Быстрее некуда! Выделение занимает лишь несколько машинных тактов, а освобождение происходит мгновенно. Конечно, этот код не завоюет приза на олимпиаде по С++, но я видел, как он всего за несколько часов работы спасал проекты с серьезными проблемами быстродействия. Как минимум, он поможет определить, на что лучше направить усилия по оптимизации, поскольку выделение и освобождение памяти исключается из рассмотрения. Последовательно применяя его к разным классам, вы сможете установить, с какими классами связаны основные затруднения.

Обратите внимание, что для выделения блоков вместо операторной функции ::operator new используется функция calloc(). Большинство компиляторов С++ выделяет большой блок с помощью функции calloc() (или функции операционной системы), а затем управляет объектами в полученном блоке. Если использовать ::operator new для выделения блоков, скорее всего, дело кончится двойными затратами и двойной фрагментацией, поскольку эти блоки будут существовать в стандартных блоках менеджера памяти. При нашей стратегии лучше обойти ::operator new.

Описанная стратегия также хорошо работает в программах с более продолжительным сроком жизни, которые изначально создают множество объектов некоторого класса, но удаляют их относительно редко (если вообще удаляют). Если операторы new и delete перегружаются на уровне классов, то оптимизацию можно ограничить классами, обладающими указанным свойством.

Сборка мусора на уровне поколений

Многие алгоритмы выглядят примерно так:

void Eval(Structure s)

{

// Создать локальные объекты

Eval(s.SomePart()); // Произвести вычисления для подструктуры // Удалить локальные объекты

}

207

Обход деревьев, вычисление рекурсивных выражений в языках типа Prolog — эти и многие другие рекурсивные алгоритмы имеют показанную структуру. Размещение локальных объектов в стеке может ускорить выделение и освобождение памяти, но этот вариант часто непрактичен. Альтернативный вариант — создать пул, локальный для Eval(), и уничтожить его целиком при выходе из Eval(). В области действия Eval() все временные объекты размещаются в этом пуле.

void* operator new(size_t size, Pool* p)

{

return p->Allocate(size);

}

 

 

template <class Type>

 

class PoolP {

// Указатель, использующий пул

private:

 

 

Type* pointee;

 

public:

 

 

PoolP(Pool* p) : pointee(new(p) Type) {}

~PoolP { pointee->Type::~Type(); }

// Все остальное для ведущих указателей

};

 

 

void Eval(Structure s)

 

{

 

 

Pool p;

// Объявляется в стеке!

PoolP<Foo> foo(&p);

// Использует пул

Eval(s.SomePart());

// Использует свой собственный пул

f(p, s);

// Вместо f(s); f будет использовать тот же пул

// Деструктор p уничтожает все сразу

}

Pool может представлять собой любую разновидность пула памяти. Скорее всего, это «тупой» пул, который просто выделяет память снизу вверх и не беспокоится о возвращении памяти. Умный указатель PoolP вызывает деструктор указываемого объекта, но не освобождает его память. На эту тему существует множество вариантов:

Обойтись без PoolP. Либо пусть пул сам вызывает деструкторы содержащихся в нем объектов из своего собственного деструктора (для этого все они должны иметь общий базовый класс и виртуальный деструктор), либо вообще не вызывайте деструкторы. (О господи! Неужели я сказал это? Но довольно часто такой вариант работает; главное — не хвастайтесь этим подвигом на семинарах по С++.)

Назначить «текущий пул» в глобальной переменной или статической переменной класса, а затем перегрузить оператор new для использования текущего пула, каким бы он ни был. При этом вам не придется передавать текущий пул всем подфункциям вроде упоминавшейся выше f().

Предоставить средства для перемещения или копирования объектов из локального пула в то место, где они смогут жить вне области действия Eval(). Эта тема выходит за рамки данной главы, а возможно, и книги, но для нее можно приспособить методику дескрипторов из следующей главы.

Последний вариант используется в стратегиях управления памятью настолько сложных, что голова начинает болеть заранее, еще до подробного знакомства с темой. К счастью, все трудности обусловлены необходимостью выбора — стоит или не стоит перемещать объекты из-за возможных обращений к ним со стороны чего-то, пережившего данный блок. Этот вопрос не из области С++, он скорее относится к алгоритмам и структурам данных.

208

Скрытая информация

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

void* Foo::operator new(size_t bytes)

{

size_t real_bytes = bytes + sizeof(size_t);

unsigned char* space = (unsigned char*)::operator new(real_bytes); ((size_t)space) = real_bytes;

return space + sizeof(size_t);

}

Теперь информацию о размере можно использовать в операторе delete или в любом другом месте, где вам захочется узнать настоящий размер. Тем не менее, при этой стратегии необходимо учесть ряд обстоятельств.

Лишние затраты

В зависимости от компилятора и операционной системы эта методика уже может использоваться незаметно для вас. Если вы самостоятельно добавите информацию о размере, она может продублировать уже хранящиеся сведения. Скорее всего, это произойдет при делегировании ::operator new на уровне объектов (см. выше). Такая методика приносит наибольшую пользу в сочетании с блочными схемами выделения памяти, она сокращает затраты ::operator new или calloc для блока, а не для отдельного объекта.

Оптимизация размера кванта

Большое преимущество этой методики заключается в том, что вы можете выделить больше места, чем было запрошено. Как это? Может ли принести пользу выделение лишней неиспользуемой памяти? На самом деле может, если подойти к этому разумно. Один из возможных вариантов — всегда выделять память с приращением в n байт, где минимальное значение n равно 4. При это повышается вероятность того, что после удаления 17-байтового объекта занимаемое им место удастся использовать, скажем, для 18или 19-байтового объекта. Более изощренный подход состоит в выделении памяти по степеням 2, или, если вы относитесь к числу истинных гурманов управления памятью, — по числам Фибоначчи. Такие системы называются системами напарников (buddy systems), поскольку для каждого выделенного блока вы сможете найти его напарника (то есть другую половинку большого блока, из которого он был выделен) исключительно по размеру и начальному адресу. Появляется возможность эффективного воссоединения соседних освобожденных блоков. Если вы интересуетесь подобными вещами, в главе 16 рассматриваются основы системы напарников для степеней 2 в контексте автоматической сборки мусора.

Другая информация

Кроме размера блока может сохраняться и другая информация, например:

адрес объекта класса для данного объекта;

флаги блока (например, «флаг зомби», о котором будет рассказано ниже);

статистическая информация — например, время создания объекта.

Списки свободных блоков

Упрощенный список свободных блоков, приведенный в предыдущей главе, может использоваться только для объектов постоянного размера. Например, он не будет нормально работать с производными классами, в которых добавляются новые переменные, поскольку они будут иметь другой размер. Сам список тоже ограничивался одним размером; для передачи оператору delete правильного размера требовались виртуальные деструкторы. Впрочем, создать более универсальные списки уже не так уж трудно.

209

Одно из несложных усовершенствований заключается в том, чтобы хранить в каждом узле списка не только адрес следующего указателя в списке, но и размер блока. Это позволит хранить в одном списке блоки разных размеров. При выделении памяти мы просматриваем список, пока не находим блок, размеры которого достаточны для наших целей. Для этого придется хранить скрытую информацию о размере блока даже после его выделения, чтобы при уничтожении объекта восстанавливать весь блок. Стратегия, прямо скажем, тупая и подходит разве что для очень маленьких списков, поскольку время поиска возрастает линейно с размером списка. Впрочем, она послужит отправной точкой для нашего обсуждения.

Более эффективное представление — коллекция списков свободной памяти, в которой каждый список представляет блоки определенного размера. Ниже показана урощенная, но полезная реализация.

class MemManager { private:

struct FreeList { // Список с блоками определенного размера

FreeList* next;

// Следующий FreeList

void* top_of_list;

// Верх текущего списка

size_t chunk_size;

// Размер каждого свободного блока

FreeList(FreeList* successor, size_t size) : next(successor),

top_of_list(NULL), chunk_size(size) {}

};

 

FreeList* all_lists;

// Список всех FreeList

public:

 

MemManager() : all_lists(NULL) {} void* Allocate(size_t bytes);

void Deallocate(void* space, size_t bytes);

};

void* MemManager::Allocate(size_t bytes)

{

for (FreeList* fl = all_lists;

fl != NULL && fl->chunk_size != bytes; fl = fl->next)

{

if (fl->top_of_list != NULL)

{

void* space = fl->top_of_list;

fl->top_of_list = *((void**)(fl->top_of_list));

return space;

 

}

 

return ::operator new(bytes);

// Пустой список

}

 

return ::operator new(bytes); // Такого списка нет

}

void MemManager::Deallocate(void* space, size_t bytes)

{

FreeList*

fl = NULL;

for (fl = all_lists; fl != NULL; fl = fl->next)

if (fl->chunk_size == bytes) break;

if (fl == NULL)

// Списка для такого размера нет

{

 

 

fl = new FreeList(all_lists, bytes); all_lists = fl;

Соседние файлы в предмете Программирование на C++