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

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

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

Спасибо

Ответы [ 16 ]

41 голосов
/ 25 сентября 2008

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

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

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

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

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

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

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

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

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

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

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

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

16 голосов
/ 25 сентября 2008

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

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

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

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

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

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

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

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

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

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

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

-Adam

2 голосов
/ 25 сентября 2008

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

2 голосов
/ 25 сентября 2008

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

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

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

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

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

1 голос
/ 25 сентября 2008

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

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

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

0 голосов
/ 25 сентября 2008

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

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

0 голосов
/ 25 сентября 2008
Встраивание

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

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

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

0 голосов
/ 25 сентября 2008

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

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