Учитывая все требования, предъявляемые к менеджерам памяти, неудивительно, что поставляемые с компиляторами операторы 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_cast
*(reinterpret_cast
signature;
// вернуть указатель на память сразу за начальной сигнатурой
return static_cast
}
Большинство недостатков этой версии оператора new связаны с тем, что он не вполне соответствует соглашениям C++ относительно функций с таким именем. Например, в правиле 51 объясняется, что все операторы new должны включать цикл вызова функции-обработчика new, чего этот вариант не делает. Этому соглашению посвящено правило 51, поэтому сейчас я хочу сосредоточиться на более тонком моменте:
Многие компьютерные архитектуры требуют, чтобы данные конкретных типов были размещены в памяти по вполне определенным адресам. Например, архитектура может требовать, чтобы указатели размещались по адресам, кратным четырем (то есть были выровнены на границу четырехбайтового слова), а данные типа double начинались с адреса, кратного восьми. Если не придерживаться этого соглашения, то возможны аппаратные ошибки во время исполнения. Другие архитектуры более терпимы, хотя и могут демонстрировать более высокую производительность, если удовлетворены требования выравнивания. Например, на архитектуре Intel x86 значения типа double могут быть выровнены по границе любого байта, но доступ к ним будет значительно быстрее, если они выровнены по восьмибайтовым границам.