• Чтобы повысить эффективность.Версии операторов new и delete, поставляемые вместе с компилятором, универсальны. Они должны быть приемлемы как для долго работающих программ (например, Web-серверов), так и для программ, работающих менее одной секунды. Они должны уметь обрабатывать серии запросов на выделение больших блоков памяти, малых блоков, а также смеси тех и других. Они должны адаптироваться к широкому диапазону вариантов использования – от динамического выделения нескольких блоков большого размера, которые существуют на протяжении всего времени работы программы, до выделения и освобождения памяти для большого количества мелких объектов с малым временем жизни. Они должны предотвращать фрагментацию «кучи», ибо если этого не делать, то в конце концов будет невозможно удовлетворить запрос на выделение большого блока памяти, даже если суммарно такой объем имеется, но разнесен по множеству мелких участков.
Учитывая все требования, предъявляемые к менеджерам памяти, неудивительно, что поставляемые с компиляторами операторы new и delete придерживаются усредненной стратегии. Они работают достаточно хорошо для всех, но оптимально – ни для кого. Если вы хорошо представляете, как динамическая память используется в вашей программе, вы сможете написать собственные версии операторов new и delete, превосходящие по эффективности стандартные. Под «превосходством» я подразумеваю, что они работают быстрее (иногда на много порядков) и требуют меньше памяти (до 50 %). Для некоторых, но отнюдь не для всех, приложений замена поставляемых new и delete собственными версиями – простой способ ощутимого повышения производительности.
• Чтобы собирать статистику использования.Прежде чем перейти к написанию собственных new и delete, благоразумно собрать информацию о том, как ваша программа использует динамическую память. Как распределены выделяемые блоки по размерам? Как распределяется их время жизни? Порядок выделения и освобождения в основном следует принципу FIFO («первым вошел – первым вышел») или же LIFO («последним вошел – первым вышел»)? Или никакой закономерности не наблюдается? Изменяется ли характер использования памяти со временем, то есть существует ли разница в порядке выделения-освобождения памяти между разными стадиями исполнения? Какой максимальный объем динамически выделенной памяти используется в каждый момент времени?
По существу, написание пользовательских версий new и delete – довольно простая задача. Например, рассмотрим вкратце, как можно реализовать глобальный оператор new с контролем записи за границами выделенного блока. Правда, в нем есть множество дефектов, но пока не будем обращать на них внимания.
static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;
// в этом коде есть несколько дефектов – см. ниже
void *operator new(std:size_t size) throw(std::bad_alloc)
{
using namespace std;
size_t realSize=size+2*sizeof(int); // увеличить размер запрошенного
// блока, чтобы можно было разместить
// сигнатуры
void *pMem = malloc(realSize); // вызвать malloc для получения памяти
if(!pMem) throw(bad_alloc);
// записать сигнатуру в первое и последнее слово выделенного блока
*(static_castpMem)) = signature;
*(reinterpret_cast(static_cast(pMem)+realSize-sizeof(int))) =
signature;
// вернуть указатель на память сразу за начальной сигнатурой
return static_cast(pMem)+sizeof(int);
}
Большинство недостатков этой версии оператора new связаны с тем, что он не вполне соответствует соглашениям C++ относительно функций с таким именем. Например, в правиле 51 объясняется, что все операторы new должны включать цикл вызова функции-обработчика new, чего этот вариант не делает. Этому соглашению посвящено правило 51, поэтому сейчас я хочу сосредоточиться на более тонком моменте: выравнивании.
Многие компьютерные архитектуры требуют, чтобы данные конкретных типов были размещены в памяти по вполне определенным адресам. Например, архитектура может требовать, чтобы указатели размещались по адресам, кратным четырем (то есть были выровнены на границу четырехбайтового слова), а данные типа double начинались с адреса, кратного восьми. Если не придерживаться этого соглашения, то возможны аппаратные ошибки во время исполнения. Другие архитектуры более терпимы, хотя и могут демонстрировать более высокую производительность, если удовлетворены требования выравнивания. Например, на архитектуре Intel x86 значения типа double могут быть выровнены по границе любого байта, но доступ к ним будет значительно быстрее, если они выровнены по восьмибайтовым границам.
Читать дальше
Конец ознакомительного отрывка
Купить книгу