Иногда компиляторы вызывают вызовы встроенных функций. Это означает, что они перемещают код вызываемой функции в вызывающую функцию. Это делает вещи немного быстрее, потому что нет необходимости вставлять и извлекать вещи из стека вызовов.
Итак, мой вопрос: почему компиляторы не встроены во все? Я предполагаю, что это сделает исполняемый файл заметно быстрее.
Единственная причина, по которой я могу придумать, - это значительно больший исполняемый файл, но действительно ли это важно в наши дни с сотнями ГБ памяти? Разве улучшенная производительность не стоит того?
Есть ли какая-то другая причина, почему компиляторы не просто встроили все вызовы функций?
optimization
compiler
Авив Кон
источник
источник
Isn't the improved performance worth it?
Для метода, который будет запускать цикл 100 раз и обрабатывать некоторые серьезные числа, издержки на перенос 2 или 3 аргументов в регистры ЦП ничего не значат.Ответы:
Во-первых, обратите внимание, что одним из основных эффектов inline является то, что он позволяет проводить дальнейшую оптимизацию на сайте вызовов.
На ваш вопрос: есть вещи, которые сложно или даже невозможно встроить:
динамически связанные библиотеки
динамически определяемые функции (динамическая диспетчеризация, вызываемая через указатели на функции)
рекурсивные функции (хвостовая рекурсия)
функции, для которых у вас нет кода (но оптимизация времени соединения позволяет это для некоторых из них)
Тогда вкладывание имеет не только благотворное влияние:
больший исполняемый файл означает больше места на диске и большее время загрузки
больший размер исполняемого файла означает увеличение нагрузки на кеш (обратите внимание, что встраивание достаточно маленьких функций, таких как простые методы получения, может уменьшить размер исполняемого файла и нагрузку на кеш)
И, наконец, для функций, выполнение которых занимает не тривиальное время, усиление просто не стоит боли.
источник
Основным ограничением является полиморфизм во время выполнения. Если при записи происходит динамическая диспетчеризация,
foo.bar()
то невозможно встроить вызов метода. Это объясняет, почему компиляторы не все встроены.Рекурсивные вызовы также не могут быть легко встроены.
Кросс-модульное встраивание также сложно выполнить по техническим причинам (например, добавочная перекомпиляция была бы невозможна)
Тем не менее, компиляторы делают много вещей.
источник
Во-первых, вы не всегда можете встроить, например, рекурсивные функции не всегда могут быть встроены (но программа, содержащая рекурсивное определение
fact
с использованием только печати,fact(8)
может быть встроена).Тогда встраивание не всегда выгодно. Если компилятор встроит так много, что результирующий код будет достаточно большим, чтобы его горячие части не помещались, например, в кэш инструкций L1, он может быть намного медленнее, чем не встроенная версия (которая легко помещалась бы в кэш L1) ... Кроме того, недавние процессоры очень быстро выполняют
CALL
машинную инструкцию (по крайней мере, в известное место, то есть прямой вызов, а не указатель вызова через).Наконец, полное встраивание требует анализа всей программы. Это может быть невозможно (или слишком дорого). В C или C ++, скомпилированном GCC (а также в Clang / LLVM ), вам необходимо включить оптимизацию во время компоновки (путем компиляции и компоновки, например, с помощью
g++ -flto -O2
), и это занимает довольно много времени компиляции.источник
Как это ни удивительно, но встраивание всего не обязательно сокращает время выполнения. Увеличенный размер вашего кода может усложнить процессору одновременное хранение всего вашего кода в кеше. Отсутствие кэша в вашем коде становится более вероятным, а потеря кэша - дорогой. Это становится намного хуже, если ваши потенциально встроенные функции велики.
Время от времени я заметно улучшал производительность, вынимая большие куски кода, помеченные как «встроенные», из заголовочных файлов, помещая их в исходный код, чтобы код находился только в одном месте, а не на каждом сайте вызовов. Тогда кэш процессора используется лучше, и вы также получаете лучшее время компиляции ...
источник
Выделение всего означало бы не только увеличение потребления дисковой памяти, но и увеличение потребления внутренней памяти, что не так много. Помните, что код также полагается на память в сегменте кода; если функция вызывается из 10000 мест (скажем, из стандартных библиотек в довольно большом проекте), то код этой функции занимает в 10000 раз больше внутренней памяти.
Другой причиной могут быть компиляторы JIT; если все встроено, то нет динамических точек для динамической компиляции.
источник
Во-первых, есть простые примеры, когда вставка всего будет работать очень плохо. Рассмотрим этот простой C-код:
Угадай, что будет делать с тобой все это?
Далее вы делаете предположение, что встраивание сделает вещи быстрее. Это иногда так, но не всегда. Одна из причин заключается в том, что код, который помещается в кэш инструкций, работает намного быстрее. Если я вызываю функцию из 10 мест, я всегда запускаю код, который находится в кэше инструкций. Если он встроен, то копии повсюду и работают намного медленнее.
Есть и другие проблемы: встраивание производит огромные функции. Огромные функции намного сложнее оптимизировать. Я получил значительный выигрыш в коде, критичном к производительности, скрыв функции в отдельный файл, чтобы компилятор не мог их встроить. В результате сгенерированный код для этих функций был намного лучше, когда они были скрыты.
КСТАТИ. У меня нет «сотен ГБ памяти». Мой рабочий компьютер даже не имеет "сотни ГБ на жестком диске". И если мое приложение, где «сотни ГБ памяти», заняло бы 20 минут, просто загрузить приложение в память.
источник