Когда "inline" неэффективен? (в С)

Некоторым людям нравится использовать ключевое слово inline в C и помещать большие функции в заголовки. Когда вы считаете, что это неэффективно? Я считаю, что это когда-то даже раздражает, потому что это необычно.

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

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

Спасибо за ваши ответы!

Ответы

Ответ 1

inline выполняет две вещи:

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

# 1 Может быть очень полезным (например, поместить определение в заголовок, если оно короткое), даже если # 2 отключен.

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


[EDIT: полные ссылки и соответствующий текст]

Два вышеуказанных пункта следуют из стандарта ISO/ANSI (ISO/IEC 9899: 1999 (E), обычно называемого "C99" ).

В §6.9 "Внешнее определение", пункт 5:

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

В то время как эквивалентное определение в С++ явно называется One Definition Rule (ODR), оно служит той же цели. Внешние элементы (т.е. Не "статические" и, следовательно, локальные для одного модуля перевода, как правило, одного исходного файла) могут быть определены только один раз, только если это не функция и встроенный.

В §6.7.4 "Спецификаторы функций" определено ключевое слово inline:

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

И сноска (ненормативная), но дает разъяснения:

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

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

(Все акценты в кавычках из стандарта.)


ИЗМЕНИТЬ 2: Несколько примечаний:

  • Существуют различные ограничения для внешних встроенных функций. У вас не может быть статической переменной в этой функции, и вы не можете ссылаться на объекты/функции объектов статической TU.
  • Только что видел это на VС++ " оптимизация всей программы", что является примером компилятор делает свою собственную встроенную вещь, а не автора.

Ответ 2

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

Ответ 3

Пример, иллюстрирующий преимущества встроенного. sinCos.h:

int16 sinLUT[ TWO_PI ]; 

static inline int16_t cos_LUT( int16_t x ) {
    return sin_LUT( x + PI_OVER_TWO )
}

static inline int16_t sin_LUT( int16_t x ) {
    return sinLUT[(uint16_t)x];
}

Когда вы делаете тяжелый хруст и хотите избежать траты времени на вычисление sin/cos, вы заменяете sin/cos на LUT.

Когда вы компилируете без встроенного компилятора, компилятор не будет оптимизировать цикл, а вывод .asm покажет что-то в строках:

;*----------------------------------------------------------------------------*
;*   SOFTWARE PIPELINE INFORMATION
;*      Disqualified loop: Loop contains a call
;*----------------------------------------------------------------------------*

Когда вы компилируете inline, компилятор знает о том, что происходит в цикле, и будет оптимизирован, потому что он точно знает, что происходит.

Выход .asm будет иметь оптимизированный конвейерный конвейер (т.е. он попытается полностью использовать все процессоры ALU и попытаться поддерживать полный конвейер процессора без NOPS).


В этом конкретном случае я смог увеличить свою производительность примерно на 2X или 4X, что привело меня к тому, что мне нужно для моего реального времени.


p.s. Я работал над процессором с фиксированной точкой... и любые операции с плавающей запятой, такие как sin/cos, убили мою производительность.

Ответ 4

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

Конечно, этот аргумент применяется только в том случае, если эта функция является частью вашего общедоступного API.

Ответ 5

Inline не работает, когда вы используете указатель на функцию.

Ответ 6

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

Кроме того, я не могу представить, почему вы его используете.

Ответ 7

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

Ответ 8

Inline может использоваться для небольших и часто используемых функций, таких как метод getter или setter. Для больших функций не рекомендуется использовать встроенный, поскольку он увеличивает размер exe. Также для рекурсивных функций, даже если вы делаете inline, компилятор будет игнорировать его.

Ответ 9

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

Ответ 10

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

Это еще один пример предопределенной оптимизации, о которой Кнут предупреждал.

Ответ 11

  • inline действует только как подсказка.
  • Добавлен только совсем недавно. Таким образом, работает только с новейшими стандартными совместимыми компиляторами.

Ответ 12

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

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

поэтому если:

static inline unsigned int foo (const char * bar)

.. не улучшает вещи по сравнению с static int foo() его время, чтобы пересмотреть ваши оптимизации (и вероятные циклы) или спорить с вашим компилятором. Соблюдайте особую осторожность, чтобы сначала спорить с вашим компилятором, а не с людьми, которые его разрабатывают.. или просто задерживают много неприятного чтения, когда вы открываете свой почтовый ящик на следующий день.

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