Почему встраивание считается быстрее, чем вызов функции? - PullRequest
39 голосов
/ 25 октября 2010

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

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

Дай мне знать, если я что-то забуду, спасибо!

Ответы [ 16 ]

60 голосов
/ 25 октября 2010

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

Для одного простого примера эта функция

void foo(bool b) {
  if (b) {
    // something
  }
  else {
    // something else
  }
}

потребует фактического ветвления, если вызывается как не встроенная функция

foo(true);
...
foo(false);

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

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

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

26 голосов
/ 25 октября 2010

«Несколько нажатий и переход к вызову функции, неужели так много издержек?»

Это зависит от функции.

Если тело функции - только одна инструкция машинного кода, издержки на вызов и возврат могут составлять много-много сотен процентов. Скажем, 6 раз, 500% накладных расходов. Затем, если ваша программа состоит только из нескольких миллиардов вызовов этой функции, без вставки вы увеличиваете время выполнения на 500%.

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

Таким образом, ответ всегда заключается в том, что касается оптимизации, прежде всего ИЗМЕРЕНИЯ.

12 голосов
/ 25 октября 2010

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

Однако, не беспокойтесь о вставке, если ваш код вызывается только несколько раз в вашем приложении.Очень беспокоюсь, если его вызывают миллионы раз, пока пользователь ждет ответов!

11 голосов
/ 25 октября 2010

Классическим кандидатом на встраивание является аксессор, например std::vector<T>::size().

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

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

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


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

5 голосов
/ 25 октября 2010

Рассмотрим простую функцию, такую ​​как:

int SimpleFunc (const int X, const int Y)
{
    return (X + 3 * Y); 
}    

int main(int argc, char* argv[])
{
    int Test = SimpleFunc(11, 12);
    return 0;
}

Это преобразуется в следующий код (MSVC ++ v6, отладка):

10:   int SimpleFunc (const int X, const int Y)
11:   {
00401020   push        ebp
00401021   mov         ebp,esp
00401023   sub         esp,40h
00401026   push        ebx
00401027   push        esi
00401028   push        edi
00401029   lea         edi,[ebp-40h]
0040102C   mov         ecx,10h
00401031   mov         eax,0CCCCCCCCh
00401036   rep stos    dword ptr [edi]

12:       return (X + 3 * Y);
00401038   mov         eax,dword ptr [ebp+0Ch]
0040103B   imul        eax,eax,3
0040103E   mov         ecx,dword ptr [ebp+8]
00401041   add         eax,ecx

13:   }
00401043   pop         edi
00401044   pop         esi
00401045   pop         ebx
00401046   mov         esp,ebp
00401048   pop         ebp
00401049   ret

Вы можете видеть, что есть только 4 инструкции для тела функции, но 15 инструкций только для служебных функций, не включая еще 3 для вызова самой функции. Если все инструкции занимают одно и то же время (но не делают), тогда 80% этого кода являются служебными.

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

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

5 голосов
/ 25 октября 2010

пусть

int sum(const int &a,const int &b)
{
     return a + b;
}
int a = sum(b,c);

равно

int a = b + c

Без прыжка - без накладных расходов

4 голосов
/ 25 октября 2010

Существует несколько причин ускорения встраивания, только одна из которых очевидна:

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

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

3 голосов
/ 25 октября 2010

Типичным примером того, где это имеет большое значение, является std :: sort, который равен O (N log N) для его функции сравнения.

Попробуйте создать вектор большого размера и вызвать std:Сначала выполните сортировку с помощью встроенной функции, а затем с помощью встроенной функции и измерьте производительность.

Кстати, именно здесь сортировка в C ++ быстрее, чем qsort в C, для чего требуется указатель на функцию.

2 голосов
/ 26 октября 2010

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

Если тело функции состоит из нескольких инструкций, то код пролога / эпилога (push / pop /инструкции вызова, в принципе) могут быть на самом деле дороже, чем само тело функции.Если вы часто вызываете такую ​​функцию (скажем, из замкнутого цикла), то, если функция не встроена, вы можете в конечном итоге тратить большую часть своего ЦП на вызов функции, а не на фактическое содержимое функции.

На самом деле важна не стоимость вызова функции в абсолютном выражении (где это может занять всего 5 тактов или что-то в этом роде), а то, сколько времени это занимает относительно того, как часто вызывается функция.Если функция настолько коротка, что ее можно вызывать каждые 10 тактов, то тратить 5 циклов на каждый вызов «ненужных» инструкций push / pop довольно плохо.

2 голосов
/ 25 октября 2010

(и стоит того, чтобы он был встроен)

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

int getData()
{
   return data ;
}

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

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

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

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

...