Категории
Самые читаемые
RUSBOOK.SU » Компьютеры и Интернет » Программирование » Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Читать онлайн Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс

Шрифт:

-
+

Интервал:

-
+

Закладка:

Сделать
1 ... 62 63 64 65 66 67 68 69 70 ... 73
Перейти на страницу:

Учитывая все требования, предъявляемые к менеджерам памяти, неудивительно, что поставляемые с компиляторами операторы 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<int>pMem)) = signature;

*(reinterpret_cast<int*>(static_cast<Byte*>(pMem)+realSize-sizeof(int))) =

signature;

// вернуть указатель на память сразу за начальной сигнатурой

return static_cast<Byte*>(pMem)+sizeof(int);

}

Большинство недостатков этой версии оператора new связаны с тем, что он не вполне соответствует соглашениям C++ относительно функций с таким именем. Например, в правиле 51 объясняется, что все операторы new должны включать цикл вызова функции-обработчика new, чего этот вариант не делает. Этому соглашению посвящено правило 51, поэтому сейчас я хочу сосредоточиться на более тонком моменте: выравнивании.

Многие компьютерные архитектуры требуют, чтобы данные конкретных типов были размещены в памяти по вполне определенным адресам. Например, архитектура может требовать, чтобы указатели размещались по адресам, кратным четырем (то есть были выровнены на границу четырехбайтового слова), а данные типа double начинались с адреса, кратного восьми. Если не придерживаться этого соглашения, то возможны аппаратные ошибки во время исполнения. Другие архитектуры более терпимы, хотя и могут демонстрировать более высокую производительность, если удовлетворены требования выравнивания. Например, на архитектуре Intel x86 значения типа double могут быть выровнены по границе любого байта, но доступ к ним будет значительно быстрее, если они выровнены по восьмибайтовым границам.

Выравнивание важно, потому что C++ требует, чтобы все указатели, возвращаемые оператором new, были выровнены для любого типа данных. Функция malloc подчиняется этим же требованиям, поэтому использование указателя, возвращенного malloc, безопасно. Но в приведенном выше операторе new мы не возвращаем указатель, полученный от malloc, а возвращаем указатель, смещенный от возвращенного malloc на размер int. Нет никаких гарантий, что это безопасно! Если клиент вызовет оператор new, чтобы получить память, достаточную для размещения double (либо если мы напишем оператор new[] для выделения памяти под массив значений типа double), а потом запустим программу на машине, где int занимает 4 байта, а значения double должны быть выровнены по границам восьмибайтовых блоков, то, скорее всего, вернем неправильно выровненный указатель. Это может вызвать аварийную остановку программы. Или же просто замедлить ее работу. В любом случае, это совсем не то, что мы хотели.

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

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

Другой вариант – менеджеры памяти с открытым кодом. Они есть для многих платформ, поэтому можете скачать и попробовать. Один из таких распределителей памяти с открытым кодом – библиотека Pool из проекта Boost (см. правило 55). Библиотека Pool предлагает распределители памяти, оптимизированные для использования в одной из наиболее часто встречающихся ситуаций, где может быть оказаться полезным нестандартный менеджер памяти: распределение памяти для большого количества мелких объектов. Во многих книгах по C++, включая и ранние редакции этой, приводится код высокопроизводительного распределителя памяти для мелких объектов, но часто опускаются такие «скучные» детали, как переносимость, соглашения о выравнивании, безопасность относительно потоков и т. п. В реальные библиотеки включен гораздо более устойчивый код. Даже если вы решите написать собственные new и delete, знакомство версий с открытым кодом, вероятно, даст вам понимание тех деталей, которые отличают «почти работающие» системы от действительно работающих. Выравнивание – одна из таких деталей. Стоило бы отметить, что в отчет TR1 (см. правило 54) включена поддержка для выявления требований выравнивания, специфичных для конкретного типа.

Тема настоящего правила – вопрос о том, когда имеет смысл подменять версии new и delete по умолчанию – на глобальном уровне или на уровне класса. Теперь мы можем ответить на этот вопрос более подробно.

• Чтобы обнаруживать ошибки использования (как было сказано выше).

• Чтобы собирать статистику об использовании динамически распределенной памяти (также было сказано выше).

• Для ускорения процесса распределения и освобождения памяти. Распределители общего назначения часто (хотя и не всегда) работают намного медленнее, чем оптимизированные версии, особенно если последние специально разработаны для объектов определенного типа. Специфичные для класса распределители являются примерами выделения блоков фиксированного размера, вроде тех, что представляет библиотека Pool из проекта Boost. Если ваше приложение однопоточное, но менеджер памяти, поставляемый с компилятором, по умолчанию потокобезопасный, то вы можете получить заметный рост производительности, написав менеджер памяти для однопоточных приложений. Конечно, прежде чем решить, что нужно переписывать операторы new и delete для повышения скорости, убедитесь с помощью профилирования, что эти функции действительно являются узким местом.

• Чтобы уменьшить накладные расходы, характерные для стандартного менеджера памяти. Менеджеры памяти общего назначения часто (хотя не всегда) не только медленнее оптимизированных версий, но и потребляют больше памяти. Это происходит из-за того, что с каждым выделенным блоком связаны некоторые накладные расходы. Распределители, оптимизированные для мелких объектов (как, например, Pool), позволяют почти избавиться от этих расходов.

• Чтобы компенсировать субоптимальное выравнивание в распределителях по умолчанию. Как я уже упоминал, самый быстрый доступ к значениям double на архитектуре x86 получается тогда, когда они выровнены по восьмибайтным границам. К сожалению, операторы new, поставляемые с некоторыми компиляторами, не гарантируют восьмибайтового выравнивания при динамическом выделении double. В этих случаях замена оператора new по умолчанию на специальный, который гарантирует такое выравнивание, может дать заметный рост производительности программы.

1 ... 62 63 64 65 66 67 68 69 70 ... 73
Перейти на страницу:
На этой странице вы можете бесплатно скачать Эффективное использование C++. 55 верных способов улучшить структуру и код ваших программ - Скотт Майерс торрент бесплатно.
Комментарии
Открыть боковую панель
Комментарии
Сергій
Сергій 25.01.2024 - 17:17
"Убийство миссис Спэнлоу" от Агаты Кристи – это великолепный детектив, который завораживает с первой страницы и держит в напряжении до последнего момента. Кристи, как всегда, мастерски строит