Правило 30: Тщательно обдумывайте использование встроенных функций
Правило 30: Тщательно обдумывайте использование встроенных функций
Встроенные функции – какая замечательная идея! Они выглядят подобно функциям, они работают подобно функциям, они намного лучше макросов (см. правило 2). Их можно вызывать, не опасаясь накладных расходов, связанных с вызовом обычных функций. Чего еще желать?
В действительности вы получаете больше, чем рассчитывали, потому что возможность избежать затрат на вызов функции – это только полдела. Оптимизация, выполняемая компилятором, обычно наиболее эффективна на участке кода, не содержащем вызовов функций. Таким образом, вы даете компилятору возможность оптимизации тела встроенной функции в зависимости от объемлющего контекста. При использовании «обычного» функционального вызова большинство компиляторов такой оптимизации на обычных не выполняют.
Все же давайте не будем слишком увлекаться. В программировании, как и в реальной жизни, не бывает «бесплатных завтраков», и встроенные функции – не исключение. Идея их использования состоит в замене каждого вызова такой функции ее телом. Не нужно быть доктором математических наук, чтобы заметить, что это увеличит общий размер вашего объектного кода. Слишком частое применение встроенных функций на машинах с ограниченной памятью может привести к созданию программы, которая превосходит доступную память. Даже при наличии виртуальной памяти «разбухание» кода, вызванное применением встроенных функций, может привести к дополнительному обмену с диском, уменьшить коэффициент попадания команд в кэш и, следовательно, снизить производительность программы.
С другой стороны, если тело встроенной функции очень короткое, то сгенерированный для нее код может быть короче кода, сгенерированного для вызова функции. В таком случае встраивание функции может привести к уменьшению объектного кода и повышению коэффициента попаданий в кэш!
Имейте в виду, что директива inline – это совет, а не команда компилятору. Совет может быть сформулирован явно или неявно. Неявный способ заключается в определении встроенной функции внутри определения класса:
class Person {
public:
...
int age() const { return theAge;} // неявный запрос на встраивание;
... // функция age определена внутри класса
private:
int theAge;
};
Такие функции обычно являются функциями-членами, но в правиле 46 объясняется, что функции-друзья тоже могут быть определены внутри класса. В этом случае они также неявно считаются встроенными.
Явно объявить встроенную функцию можно, предварив ее определение ключевым словом inline. Например, вот как обычно реализован стандартный шаблон max (из заголовочного файла <algorithm>):
template <typename T> // явный запрос на
inline const T& std::max(const T& a, const T& b) // встраивание: функции
{ return a < b ? b : c;} // std::max предшествует
// слово inline
Тот факт, что max – это шаблон, наводит на мысль, что встроенные функции и шаблоны обычно объявляются в заголовочных файлах. Некоторые программисты делают из этого вывод, что шаблоны функций обязательно должны быть встроенными. Это заключение одновременно неверно и потенциально опасно, поэтому рассмотрим его внимательнее.
Встроенные функции обычно должны находиться в заголовочных файлах, поскольку большинство разработки программ выполняют встраивание во время компиляции. Чтобы заменить вызовы функции встраиванием ее тела, компилятор должен увидеть эту функцию. (Некоторые среды могут встраивать функции во время компоновки, а есть и такие – например, среды разработки на базе. NET Common Language Infrastructure (CLI), – которые осуществляют встраивание во время исполнения. Но это скорее исключение, чем правило. Встраивание функций в большинстве программ на C++ происходит во время компиляции.)
Шаблоны обычно находятся в заголовочных файлах, потому что компилятор должен знать, как шаблон выглядит, чтобы конкретизировать его в момент использования. (Но и это правило не является универсальным. Некоторые среды разработки выполняют конкретизацию шаблонов во время компоновки. Однако конкретизация на этапе компиляции встречается чаще.)
Конкретизация шаблонов никак не связана со встраиванием. Если вы полагаете, что все функции, конкретизированные из вашего шаблона, должны быть встроенными, объявите шаблон встроенным (inline); именно так разработчики стандартной библиотеки поступили с шаблоном std::max (см. пример выше). Но если вы пишете шаблон для функции, которую нет смысла делать встроенной, не объявляйте встроенным и ее шаблон (явно или неявно). Встраивание обходится дорого, и вряд ли вы захотите платить за это без должного размышления. Мы уже упоминали, что встраивание раздувает код (особенно это важно при разработке шаблонов – см. правило 44), но есть и другие затраты, которые мы скоро обсудим.
Но прежде напомним, что встраивание – это совет, который компилятор может проигнорировать. Большинство компиляторов отвергают встраивание функций, которые представляются слишком сложными (например, содержат циклы или рекурсию), и за исключением наиболее тривиальных случаев, вызов виртуальной функции отменяет встраивание. В этом нет ничего удивительного: virtual означает «какую точно функцию вызвать, определяется в момент исполнения», а inline – «перед исполнением заменить вызов функции ее кодом». Если компилятор не знает, какую функцию вызывать, то трудно винить его в том, что он отказывается делать встраивание.
Все это в конечном счете сводится к следующему: от реализации используемого компилятора зависит, встраивается ли в действительность встроенная функция. К счастью, большинство компиляторов обладают достаточными диагностическими возможностями и выдают предупреждение (см. правило 53), если не могут выполнить запрошенное вами встраивание.
Иногда компилятор генерирует тела встроенной функции, даже если ничто не мешает ее встроить. Например, если ваша программа получает адрес встроенной функции, то компилятор, как правило, должен сгенерировать настоящее тело функции. Как иначе он может получить адрес функции, если ее не существует? В совокупности с тем фактом, что обычно компиляторы не выполняют встраивание, если функция вызывается по указателю, это значит, что вызовы встроенных функций могут встраиваться или не встраиваться в зависимости от того, как к ней производится обращение:
inline void f() {...} // предположим, что компилятор может встроить вызовы f
void (*pf)() = f; // pf указывает на f
...
f(); // этот вызов будет встроенным, потому что он
// «нормальный»
pf(); // этот вызов, вероятно, не будет встроен, потому что
// функция вызвана по указателю
Призрак невстраиваемых inline-функций может преследовать вас, даже если вы никогда не используете указателей на функции, потому что указатели на функции может запрашивать не только программист. Иногда компилятор генерирует невстраиваемые копии конструкторов и деструкторов так, что они запрашивают указатели на функции во время конструирования и разрушения объектов в массивах.
Фактически конструкторы и деструкторы часто являются наихудшими кандидатами для встраивания. Например, рассмотрим конструктор класса Derived:
class Base {
public:
...
private:
std::string bm1, bm2; // члены базового класса 1 и 2
};
class Derived: public Base {
public:
Derived(){} // конструктор Derived пуст – не так ли?
...
private:
std::string dm1, dm2, dm3; // члены производного класса 1–3
};
Этот конструктор выглядит как отличный кандидат на встраивание, поскольку он не содержит никакого кода. Но впечатление обманчиво.
C++ дает различные гарантии о том, что должно происходить при конструировании и разрушении объектов. Например, когда вы используете оператор new, динамически создаваемые объекты автоматически инициализируются своими конструкторами, а при обращении к delete вызываются соответствующие деструкторы. Когда вы создаете объект, то автоматически конструируются члены всех его базовых классов, а равно его собственные данные-члены, а во время удаления объекта автоматически происходит обратный процесс. Если во время конструирования объекта возбуждается исключение, то все части объекта, которые были к этому моменту сконструированы, автоматически разрушаются. Во всех этих случаях C++ говорит, что должно случиться, но не говорит – как. Это зависит от реализации компилятора, но должно быть понятно, что такие вещи не происходят сами по себе. В вашей программе должен быть какой-то код, который все это реализует, и этот код, который генерируется компилятором и вставляется в вашу программу, должен где-то находиться. Иногда он помещается в конструкторы и деструкторы, поэтому можем представить себе следующую реализацию сгенерированного кода в якобы пустом конструкторе класса Derived:
Derived::Derived() // концептуальная реализация
{ // «пустого» конструктора класса Derived
Base::Base(); // инициализировать часть Base
try {dm1.std::string::string();} // попытка сконструировать dm1
catch(…) { // если возбуждается исключение,
Base::~Base(); // разрушить часть базового класса
throw; // распространить исключение выше
}
try {dm2.std::string::string();} // попытка сконструировать dm2
catch(…){ // если возбуждается исключение,
dm1.std::string::~string(); // разрушить dm1
Base::~Base(); // разрушить часть базового класса
throw; // распространить исключение
}
try {dm3.std::string::string();} // сконструировать dm3
catch(…){ // если возбуждается исключение,
dm2.std::string::~string(); // разрушить dm2
dm1.std::string::~string(); // разрушить dm1
Base::~Base(); // разрушить часть базового класса
throw; // распространить исключение
}
}
В действительности это не совсем тот код, который порождает компилятор, потому что реальные компиляторы обрабатывают исключения более сложным образом. И все же этот пример довольно точно отражает поведение «пустого» конструктора класса Derived. Независимо от того, насколько хитроумно обходится с исключениями компилятор, конструктор Derived должен, по крайней мере, вызывать конструкторы своих данных-членов и базового класса, и эти вызовы (которые сами по себе могут быть встроенными) могут свести преимущества встраивания на нет.
То же самое относится и к конструктору класса Base, поэтому если он встроенный, то весь вставленный в него код вставляется также и в конструктор Derived (поскольку конструктор Derived вызывает конструктор Base). И если конструктор класса string тоже окажется встроенным, то в конструктор Derived его код войдет пять раз – по одному для каждой из пяти имеющихся в классе Derived строк (две унаследованные и три, объявленные в нем самом). Наверное, теперь вам ясно, почему решений о встраивании конструктора Derived не стоит принимать с легким сердцем. Аналогично обстоят дела и с деструктором класса Derived, который каким-то образом должен гарантировать правильное уничтожение всех объектов, инициализированных конструктором.
Разработчики библиотек должны принимать во внимание, что произойдет при объявлении функций встроенными, потому что невозможно предоставить двоичное обновление видимых клиенту встроенных библиотечных функций. Другими словами, если f – встроенная библиотечная функция, то пользователи этой библиотеки встраивают ее тело в свои приложения. Если разработчик библиотеки позднее решит изменить f, то все программы, которые ее использовали, придется откомпилировать заново. Часто это нежелательно. С другой стороны, если f не будет встроенной функцией, то после ее модификации клиентские программы нужно будет лишь заново компоновать с библиотекой. Это ощутимо быстрее, чем перекомпиляция, а если библиотека, содержащая функцию, является динамической, то изменения в ней вообще будут прозрачны для пользователей.
При разработке программ важно иметь в виду все эти соображения, но с практической точки зрения наиболее существен следующий факт: у большинства отладчиков возникают проблемы со встроенными функциями. Это совсем не удивительно. Как установить точку остановки в функции, которой не существует? Хотя некоторые среды разработки ухитряются поддерживать отладку встроенных функций, во многих встраивание для отладочных версий просто отключается.
Это приводит нас к следующей стратегии выбора функций, подходящих для встраивания. Поначалу откажитесь от встроенных функций вовсе, или, по крайней мере, ограничьтесь теми, которые обязаны быть встроенными (см. правило 46) либо являются тривиальными (такие как Person::age выше). Применяя встроенные функции с должной аккуратностью, вы не только получаете возможность пользоваться отладчиком, но и определяете встраиванию подобающее место: тонкая оптимизация вручную. Не забывайте об эмпирическом правиле «80–20», которое утверждает, что типичная программа тратит 80 % времени на исполнение 20 % кода. Это важное правило, поскольку оно напоминает, что цель разработчика программного обеспечения – идентифицировать те 20 % кода, которые действительно способны повысить производительность программы. Можно до бесконечности оптимизировать и объявлять функции inline, но все это будет пустой тратой времени, если только вы не сосредоточите усилия на нужных функциях.
Что следует помнить
• Делайте встраиваемыми только небольшие, часто вызываемые функции. Это облегчит отладку, даст возможность выполнять обновления библиотек на двоичном уровне, уменьшит эффект «разбухания» кода и поможет повысить быстродействие программы.
• Не объявляйте шаблоны функций встроенными только потому, что они появляются в заголовочных файлах.
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКДанный текст является ознакомительным фрагментом.
Читайте также
9.1.6.2. Использование функций BSD: wait3() и wait4()
9.1.6.2. Использование функций BSD: wait3() и wait4() Системные вызовы BSD wait3() и wait4() полезны, если вы интересуетесь ресурсами, использованными порожденным процессом. Функции нестандартны (что означает, что они не являются частью POSIX), но широко доступны, в том числе на GNU/Linux. Объявления
Пример: использование функций gethostbyname и getservbyname
Пример: использование функций gethostbyname и getservbyname Теперь мы можем изменить код нашего TCP-клиента времени и даты, показанный в листинге 1.1, так, чтобы использовать функции gethostbyname и getservbyname и принимать два аргумента командной строки: имя узла и имя службы. Наша программа
Использование функций sigsetjmp и siglongjmp
Использование функций sigsetjmp и siglongjmp Нашу проблему можно решить корректно, если отказаться от прерывания блокированного системного вызова обработчиком сигнала, вместо этого вызвав из обработчика сигнала функцию siglongjmp. Этот метод называется нелокальным оператором goto
Использование функций в операторах присваивания
Использование функций в операторах присваивания Поскольку функции и создаваемые вами процедуры типа Function возвращают значения, их тоже можно использовать в операторах присваивания. Например:ctrFavorite = InputBox(Кто сегодня фаворит?)В данном случае используется функция VBA Input Box,
Использование встроенных форматов для функции Format
Использование встроенных форматов для функции Format В табл. 11.2 приводятся именованные встроенные форматы для данных различных типов в VBA. Используйте их как аргумент в функции Format. He забудьте при этом заключить в кавычки имя выбранного вами формата (из первого столбца
Использование в scoqe встроенных функций
Использование в scoqe встроенных функций Независимо от того, пишете ли вы собственную функцию или процедуру Sub, не стесняйтесь призывать на помощь широчайшие возможности встроенных функций Excel по анализу данных и проведению вычислений. Использовать их в своем собственном
Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами
Правило 14: Тщательно продумывайте поведение при копировании классов, управляющих ресурсами В правиле 13 изложена идея Получение Ресурса Есть Инициализация (Resource Acquisition Is Initialization – RAII), лежащая в основе создания управляющих ресурсами классов. Было также показано, как эта
Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы»
Правило 45: Разрабатывайте шаблоны функций-членов так, чтобы они принимали «все совместимые типы» Интеллектуальные указатели – это объекты, которые ведут себя во многом подобно обычным указателям, но добавляют функциональность, которую последние не предоставляют.
Использование функций
Использование функций Программа Excel содержит огромное количество функций: математических, статистических, инженерных, логических, текстовых, финансовых и т. д. Эти функции вы можете использовать для создания формул или обработки данных таблицы.В данном разделе действие
Использование встроенных компонентов eVB
Использование встроенных компонентов eVB Окно настройки свойств (Properties Window)Окно настройки свойств по умолчанию располагается в правом нижнем углу экрана. Его внешний вид показан на рис. 3.18. Рис. 3.18. Окно настройки свойств.При помощи окна настройки свойств пользователь
Использование встроенных ресурсов
Использование встроенных ресурсов При работе программы необходимо получить доступ к графическому файлу из ресурсов и вывести изображение на экран. Для этого сначала необходимо получить ссылку на сборку. Соответствующий код приведен в листинге 11.1.Листинг 11.1// Получим
5.2.7. Использование функций в формулах
5.2.7. Использование функций в формулах Функцией называется операция, используемая в формуле, которая производит какие-либо действия с содержимым ячеек: вычисления, манипуляции с датами, со строками текста и т. п. Numbers включает 168 функций.Каждая функция состоит из имени и
Использование встроенных стилей
Использование встроенных стилей С помощью стилей можно установить сразу несколько параметров форматирования символов и абзацев, что позволяет сэкономить время при форматировании больших документов и обеспечить единообразное оформление различных частей