Чому компілятори не впорядковують все? [зачинено]


13

Іноді компілятори вбудовують вбудовані функції. Це означає, що вони переміщують код викликаної функції у функцію виклику. Це робить дещо швидше, тому що немає потреби натискати та висувати матеріали на стек викликів та вимикати їх.

Отже, моє запитання: чому компілятори не впорядковують все? Я припускаю, що це зробить виконуваний файл значно швидше.

Єдина причина, про яку я можу подумати - це значно більший виконуваний файл, але чи справді це має значення в наші дні з сотнями ГБ пам'яті? Невже покращена продуктивність не варта?

Чи є якась інша причина, чому компілятори не просто вбудовують всі функції викликів?


18
IDK про вас, але у мене немає сотень ГБ пам’яті, а просто лежав.
квітня

2
Isn't the improved performance worth it?Для методу, який запустить цикл 100 разів і розчавить деякі серйозні числа, накладні витрати на переміщення 2 або 3 аргументів до регістрів процесора - це нічого.
Doval

5
Ви надто загальні, чи означає "компілятори" "всі компілятори" і чи означає "все" насправді "все"? Тоді відповідь проста, виникають ситуації, коли ви просто не можете вкласти їх. На думку приходить рекурсія.
Otávio Décio

17
Місце кешу - це набагато важливіший спосіб, ніж крихітні виклики функцій.
SK-логіка

3
Чи справді покращення продуктивності насправді має значення у сотні GFLOPS процесорної потужності?
mouviciel

Відповіді:


22

По-перше, зауважте, що одним із головних ефектів вбудованого зв'язку є те, що він дозволяє проводити подальші оптимізації на сайті виклику.

Що стосується вашого питання: Є речі, які важко або навіть неможливо викласти:

  • динамічно пов'язані бібліотеки

  • динамічно визначені функції (динамічна відправка, викликана через покажчики функцій)

  • рекурсивні функції (хвостова рекурсія може)

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

Тоді вкладка має не тільки корисні ефекти:

  • більший виконуваний файл означає більше місця на диску та більший час завантаження

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

І, нарешті, для функцій, які потребують нетривіального часу на виконання, посилення просто не варто страждати.


3
деякі рекурсивні дзвінки можуть бути вбудовані (хвостові дзвінки), але всі вони можуть бути перетворені на ітерацію, якщо ви необов'язково додасте явний стек
храповий виродк

@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; якщо все в порядку, то не існує гарячих точок, які слід динамічно складати.


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 хвилин, щоб просто завантажити додаток у пам'ять.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.