Подтвердить что ты не робот

Почему встраивание считается быстрее, чем вызов функции?

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

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

Сообщите мне, если я что-то забыл, спасибо!

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

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

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

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

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

Итак, ответ всегда, когда дело доходит до оптимизации, прежде всего MEASURE.

Ответ 3

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

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

Ответ 4

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

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

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

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


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

Ответ 5

пусть

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

равно

int a = b + c

Нет прыжка - нет накладных расходов

Ответ 6

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

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;
}

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

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% этого кода являются служебными служебными данными.

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

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

Ответ 7

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

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

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

Ответ 8

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

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

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

Ответ 9

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

Ответ 10

(и стоит раздуть его встраиваемый)

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

int getData()
{
   return data ;
}

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

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

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

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

Ответ 11

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

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

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

Ответ 12

Потому что нет вызова. Код функции просто скопирован

Ответ 13

Вложение функции - это предложение компилятору заменить вызов функции с помощью определения. Если его заменить, то не будет функции, вызывающей операции стека [push, pop]. Но это не всегда гарантировано.:)

- Приветствия

Ответ 14

Оптимизация компиляторов применяет набор эвристик для определения того, выгодна ли вставка.

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

Ответ 15

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

Ответ 16

Потому что никакой переход не выполняется.