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

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

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

Спасибо

Ответ 1

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

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

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

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

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

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

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

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

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

Ответ 2

Производительность

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

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

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

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

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

Однако объявление встроенной функции с использованием ключевого слова inline имеет другие эффекты, и может быть действительно необходимо, чтобы удовлетворить правилу One Definition ( ODR): это правило в стандарте С++ указывает, что данный символ может быть объявлен несколько раз, но может быть определен только один раз. Если редактор ссылок (= линкер) встречает несколько идентичных определений символов, он будет генерировать ошибку.

Одним из решений этой проблемы является обеспечение того, чтобы блок компиляции не экспортировал данный символ, предоставив ему внутреннюю связь, объявив его 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

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

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

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

Ответ 3

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

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

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

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

Ответ 4

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

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

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

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

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

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

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

-Adam

Ответ 5

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

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

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

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

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

Ответ 6

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

Ответ 7

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

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

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

Ответ 8

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

Ответ 9

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

Ответ 10

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

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

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' при использовании этой опции.

Ответ 11

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

Ответ 12

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

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

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

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

Ответ 13

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

Ответ 14

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

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

Ответ 15

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

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

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

Ответ 16

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

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