Почему я должен когда-либо использовать встроенный код?

Я разработчик C/C++, и вот пара вопросов, которые всегда сбивали меня с толку.

  • Есть ли большая разница между "обычным" кодом и встроенным кодом?
  • Какая основная разница?
  • Является ли встроенный код просто "формой" макросов?
  • Какой компромисс должен быть сделан при выборе встроенного кода?

Спасибо

16 ответов

Решение
  • Есть ли большая разница между "обычным" кодом и встроенным кодом?

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

  • Является ли встроенный код просто "формой" макросов?

Нет! Макрос - это простая замена текста, которая может привести к серьезным ошибкам. Рассмотрим следующий код:

#define unsafe(i) ( (i) >= 0 ? (i) : -(i) )

[...]
unsafe(x++); // x is incremented twice!
unsafe(f()); // f() is called twice!
[...]

Используя встроенную функцию, вы уверены, что параметры будут оценены до ее фактического выполнения. Они также будут проверены на тип и, в конечном счете, преобразованы, чтобы соответствовать формальным типам параметров.

  • Какой компромисс должен быть сделан при выборе встроенного кода?

Обычно выполнение программы должно быть быстрее при использовании встроенных функций, но с большим двоичным кодом. Для получения дополнительной информации вы должны прочитать GoTW # 33.

Спектакль

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

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

Декларирование функций inline явно для увеличения производительности (почти?) всегда не нужно!

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

Одно Правило Определения

Тем не менее, объявление встроенной функции с помощью inline Ключевое слово имеет другие эффекты и может фактически потребоваться для удовлетворения правила единого определения (ODR). Это правило в стандарте C++ гласит, что данный символ может быть объявлен несколько раз, но может быть определен только один раз. Если редактор ссылок (= linker) встретит несколько идентичных определений символов, он выдаст ошибку.

Одним из решений этой проблемы является обеспечение того, чтобы модуль компиляции не экспортировал данный символ, предоставив ему внутреннюю связь, объявив его static,

Тем не менее, часто лучше пометить функцию inline вместо. Это говорит компоновщику объединить все определения этой функции в единицах компиляции в одно определение, с одним адресом и общими функционально-статическими переменными.

В качестве примера рассмотрим следующую программу:

// header.hpp
#ifndef HEADER_HPP
#define HEADER_HPP

#include <cmath>
#include <numeric>
#include <vector>

using vec = std::vector<double>;

/*inline*/ double mean(vec const& sample) {
    return std::accumulate(begin(sample), end(sample), 0.0) / sample.size();
}

#endif // !defined(HEADER_HPP)
// test.cpp
#include "header.hpp"

#include <iostream>
#include <iomanip>

void print_mean(vec const& sample) {
    std::cout << "Sample with x̂ = " << mean(sample) << '\n';
}
// main.cpp
#include "header.hpp"

void print_mean(vec const&); // Forward declaration.

int main() {
    vec x{4, 3, 5, 4, 5, 5, 6, 3, 8, 6, 8, 3, 1, 7};
    print_mean(x);
}

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

Теперь, если вы попытаетесь связать эти два модуля компиляции - например, с помощью следующей команды:

⟩⟩⟩ g++ -std=c++11 -pedantic main.cpp test.cpp

вы получите сообщение об ошибке "дублирующий символ __Z4meanRKNSt3__16vectorIdNS_9allocatorIdEEEE" (которое является искаженным именем нашей функции mean).

Если, однако, вы раскомментируете inline модификатор перед определением функции, код компилируется и связывается правильно.

Шаблоны функций - это особый случай: они всегда встроены независимо от того, были ли они объявлены таким образом. Это не означает, что компилятор встроит в них вызовы, но они не будут нарушать ODR. То же самое верно для функций-членов, которые определены внутри класса или структуры.

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

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

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

Редактировать: как уже отмечали другие, встраивание это только предложение для компилятора. Он может свободно игнорировать вас, если думает, что вы делаете глупые запросы, например, вставляя огромный 25-строчный метод.

  • Есть ли большая разница между "обычным" кодом и встроенным кодом?

Да - встроенный код не включает вызов функции и сохранение переменных регистра в стеке. Он использует пространство программы каждый раз, когда его "вызывают". Таким образом, в целом выполнение занимает меньше времени, поскольку в процессоре нет разветвлений и сохранения состояния, очистки кешей и т. Д.

  • Является ли встроенный код просто "формой" макросов?

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

  • Какой компромисс должен быть сделан при выборе встроенного кода?

    • Макрос: высокое использование кода, быстрое выполнение, трудно поддерживать, если "функция" длинная
    • Функция: низкое использование кода, медленное выполнение, простота обслуживания
    • Встроенная функция: высокое использование кода, быстрое выполнение, простота обслуживания

Следует отметить, что сохранение регистра и переход к функции занимает место в коде, поэтому для очень маленьких функций встроенное может занимать меньше места, чем функция.

-Адам

Inline отличается от макросов тем, что это подсказка для компилятора (компилятор может решить не включать код!), А макросы являются генерацией исходного текста перед компиляцией и, как таковые, "вынуждены" вставлять код.

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

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

Неудобство: раздувает сгенерированный бинарный файл.

Это макрос? Не совсем, потому что компилятор все еще проверяет тип параметров и т. Д.

Как насчет умных компиляторов? Они могут игнорировать встроенную директиву, если они "чувствуют", что функция слишком сложна / слишком велика. И, возможно, они могут автоматически встроить некоторые тривиальные функции, такие как простые методы получения / установки.

Маркировка функции inline означает, что компилятор имеет возможность включить в "in-line", где она вызывается, если компилятор решит это сделать; напротив, макрос всегда будет развернут на месте. Встроенная функция будет иметь соответствующие символы отладки, настроенные для того, чтобы символический отладчик мог отслеживать источник, откуда он поступил, в то время как макрос отладки вводит в заблуждение. Встроенные функции должны быть допустимыми функциями, а макросы... ну, нет.

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

Если вы доверяете своему компилятору, отметьте небольшие функции, используемые во внутренних циклах inline обильно; Компилятор будет нести ответственность за правильные действия при принятии решения о включении или отключении.

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

Обычно вам не нужно вручную решать, должна ли функция быть встроенной или нет. Например, GCC решит это автоматически в зависимости от уровня оптимизации (-Ox) и других параметров. Он будет принимать во внимание такие вещи, как "Насколько велика функция?" (количество инструкций), как часто он вызывается в коде, насколько двоичный код будет увеличиваться при его вставке, и некоторые другие метрики. Например, если функция является статической (то есть не экспортируется в любом случае) и вызывается только один раз в вашем коде, и вы никогда не используете указатель на функцию, велика вероятность, что GCC решит включить ее автоматически, так как она не окажет отрицательного воздействия (двоичный файл не станет больше, если его вставить только один раз).

Если вы помечаете свой код как встроенный в fe C++, вы также говорите своему компилятору, что код должен выполняться встроенным, т.е. этот блок кода будет "более или менее" вставлен там, где он вызывается (таким образом, удаляя нажатия, выталкивания и скачки по стеку). Итак, да... рекомендуется, если функции подходят для такого поведения.

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

Если код работает медленно, обратитесь к своему профилировщику, чтобы найти проблемы и поработать над ними.

Я перестал добавлять встроенные функции в заголовочные файлы, это увеличивает сцепление, но мало что дает взамен.

Прежде всего, inline - это запрос к компилятору, чтобы встроить функцию. Так что компилятору делать его встроенным или нет.

  1. Когда использовать? Когда функция имеет очень мало строк (для всех методов доступа и мутатора), но не для рекурсивных функций
  2. Преимущество? Время, затрачиваемое на вызов функции, не задействовано
  3. Является ли встроенный компилятор какой-либо собственной функцией? Да, когда когда-либо функция определена в заголовочном файле внутри класса

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

Встраивание обычно разрешено на уровне 3 оптимизации (-O3 в случае GCC). Это может быть значительное улучшение скорости в некоторых случаях (когда это возможно).

Явное встраивание в ваши программы может добавить некоторое улучшение скорости за счет увеличения размера кода.

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

Вы можете просто включить уровень оптимизации 3 и забыть об этом, позволяя компилятору выполнять свою работу.

Я не буду повторять вышеизложенное, но стоит отметить, что виртуальные функции не будут встроены, поскольку вызываемая функция разрешается во время выполнения.

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

Классическим примером хорошего кандидата на встраивание являются геттеры для простых конкретных классов.

CPoint
{
  public:

    inline int x() const { return m_x ; }
    inline int y() const { return m_y ; }

  private:
    int m_x ;
    int m_y ;

};

Некоторые компиляторы (например, VC2005) имеют опцию для агрессивного встраивания, и вам не нужно указывать ключевое слово "inline" при использовании этой опции.

"inline" подобен 2000-му эквиваленту "register". Не беспокойтесь, компилятор может решить, что оптимизировать лучше, чем вы.

Другие вопросы по тегам