Почему компиляторы не встроены во все? [закрыто]

13

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

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

Единственная причина, по которой я могу придумать, - это значительно больший исполняемый файл, но действительно ли это важно в наши дни с сотнями ГБ памяти? Разве улучшенная производительность не стоит того?

Есть ли какая-то другая причина, почему компиляторы не просто встроили все вызовы функций?

Авив Кон
источник
18
ИДК о вас, но у меня нет сотен ГБ памяти, просто лежащих вокруг.
Ampt
2
Isn't the improved performance worth it?Для метода, который будет запускать цикл 100 раз и обрабатывать некоторые серьезные числа, издержки на перенос 2 или 3 аргументов в регистры ЦП ничего не значат.
Довал
5
Вы слишком универсальны, означает ли «компиляторы» «все компиляторы» и действительно ли «все» означает «все»? Тогда тогда ответ прост, есть ситуации, когда вы просто не можете встроить. Рекурсия приходит на ум.
Отавио Десио
17
Локальность кэша намного важнее, чем незначительные накладные расходы на вызовы функций.
SK-logic
3
Действительно ли улучшение производительности действительно имеет значение в наши дни с сотнями GFLOPS вычислительной мощности?
Mouviciel

Ответы:

22

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

На ваш вопрос: есть вещи, которые сложно или даже невозможно встроить:

  • динамически связанные библиотеки

  • динамически определяемые функции (динамическая диспетчеризация, вызываемая через указатели на функции)

  • рекурсивные функции (хвостовая рекурсия)

  • функции, для которых у вас нет кода (но оптимизация времени соединения позволяет это для некоторых из них)

Тогда вкладывание имеет не только благотворное влияние:

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

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

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

AProgrammer
источник
3
некоторые рекурсивные вызовы могут быть встроенными (хвостовые вызовы), но все они могут быть преобразованы в итерацию, если вы дополнительно добавите явный стек
ratchet freak
@ratchetfreak, вы также можете преобразовать некоторые не хвостовые рекурсивные вызовы в хвостовые. Но это для меня в области «трудной» задачи (особенно когда у вас есть ко-рекурсивные функции или вам нужно динамически определять, куда переходить, чтобы имитировать возвращение), но это не невозможно (вы просто создаете структуру продолжения и учитывая что настоящее становится легче).
AProgrammer
11

Основным ограничением является полиморфизм во время выполнения. Если при записи происходит динамическая диспетчеризация, foo.bar()то невозможно встроить вызов метода. Это объясняет, почему компиляторы не все встроены.

Рекурсивные вызовы также не могут быть легко встроены.

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

Тем не менее, компиляторы делают много вещей.

Саймон Бергот
источник
3
Встраивание через виртуальную рассылку очень сложно, но не невозможно. Некоторые компиляторы C ++ способны делать это при определенных обстоятельствах.
bstamour
2
... а также некоторые JIT-компиляторы (девиртуализация).
Фрэнк
@bstamour Любой полуприличный компилятор любого языка с соответствующими оптимизациями будет статически отправлять, т.е. девиртуализировать, вызов объявленного виртуального метода для объекта, динамический тип которого известен во время компиляции. Это может облегчить встраивание, если фаза девиртуализации происходит до (или другого) этапа встраивания. Но это тривиально. Вы что-то еще имели в виду? Я не понимаю, каким образом можно добиться какого-либо фактического «встраивания посредством виртуальной отправки». Для инлайн, необходимо знать статический тип - т.е. devirtualise - поэтому существование встраивания средств там нет нет виртуальной диспетчеризации
underscore_d
9

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

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

Наконец, полное встраивание требует анализа всей программы. Это может быть невозможно (или слишком дорого). В C или C ++, скомпилированном GCC (а также в Clang / LLVM ), вам необходимо включить оптимизацию во время компоновки (путем компиляции и компоновки, например, с помощью g++ -flto -O2), и это занимает довольно много времени компиляции.

Василий Старынкевич
источник
1
Для записи, LLVM / Clang (и несколько других компиляторов) также поддерживает оптимизацию времени соединения .
Вы
Я знаю это; LTO существовал в прошлом веке (IIRC, по крайней мере, в некоторых проприетарных компиляторах MIPS).
Василий Старынкевич,
7

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

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

Том Таннер
источник
кажется, что это просто повторяет высказанные и объясненные замечания в предыдущем ответе, который был опубликован час назад
комнат
1
Какие кеши? L1? L2? L3? Какой из них важнее?
Питер Мортенсен
1

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

Другой причиной могут быть компиляторы JIT; если все встроено, то нет динамических точек для динамической компиляции.

m3th0dman
источник
1

Во-первых, есть простые примеры, когда вставка всего будет работать очень плохо. Рассмотрим этот простой C-код:

void f1 (void) { printf ("Hello, world\n"); }
void f2 (void) { f1 (); f1 (); f1 (); f1 (); }
void f3 (void) { f2 (); f2 (); f2 (); f2 (); }
...
void f99 (void) { f98 (); f98 (); f98 (); f98 (); }

Угадай, что будет делать с тобой все это?

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

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

КСТАТИ. У меня нет «сотен ГБ памяти». Мой рабочий компьютер даже не имеет "сотни ГБ на жестком диске". И если мое приложение, где «сотни ГБ памяти», заняло бы 20 минут, просто загрузить приложение в память.

gnasher729
источник