На скільки функціональні дзвінки впливають на продуктивність?


13

Витяг функціональності в методи або функції є обов'язковим для модуля коду, читабельності та сумісності, особливо в OOP.

Але це означає, що буде здійснено більше функцій дзвінків.

Як розбиття нашого коду на методи чи функції насправді впливає на ефективність у сучасних * мовах?

* Найпопулярніші з них: C, Java, C ++, C #, Python, JavaScript, Ruby ...



1
Я думаю, що кожна мовна реалізація, котра стоїть її солі, вже декілька десятиліть робить інлінійну. IOW, накладні витрати дорівнюють 0.
Jörg W Mittag

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

2
Як це вплине на продуктивність? Це або зробить це швидше, або повільніше, або не змінить його, залежно від того, якою конкретною мовою ви користуєтесь та якою є структура фактичного коду та, можливо, від того, яку версію компілятора ви використовуєте і, можливо, навіть на якій платформі ви ' ти знову працюєш. Кожна відповідь, яку ви отримаєте, буде певною варіацією цієї невизначеності, що містить більше слів і більше підтверджень.
GrandOpener

1
Вплив, якщо такий є, настільки малий, що ви, людина, ніколи цього не помітите. Є й інші набагато важливіші речі, про які слід потурбуватися. Як і в тому, чи має бути вкладка 5 або 7 пробілів.
MetaFight

Відповіді:


21

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

void DoSomething()
{
   a = a + 1;
   DoSomethingElse(a);
}

void DoSomethingElse(int a)
{
   b = a + 3;
}

Компілятор вирішує встроїти DoSomethingElse, і код стає

void DoSomething()
{
   a = a + 1;
   b = a + 3;
}

Якщо функції не впорядковані, так, відбувається виклик продуктивності для здійснення функціонального виклику. Однак це такий мізерний удар, що лише надзвичайно високопродуктивний код буде турбуватися про виклики функцій. А для таких проектів код зазвичай пишеться в зборі.

Виклики функцій (залежно від платформи), як правило, включають кілька інструкцій, що включають 10 секунд, і це включає збереження / відновлення стека. Деякі виклики функцій складають інструкцію про стрибок та повернення.

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

Коротше кажучи: виклики функцій можуть чи не впливати на продуктивність. Єдиний спосіб сказати - це профіль вашого коду. Не намагайтеся вгадати, де знаходяться повільні кодові плями, адже компілятор і апаратні засоби мають неймовірні хитрощі в рукавах. Профілюйте код, щоб отримати місце розташування повільних точок.


1
Я бачив із сучасними компіляторами (gcc, clang) у ситуаціях, коли мені дуже хотілося, щоб вони створили досить поганий код для циклів у великій функції . Вилучення циклу в статичну функцію не допомогло через вбудовування. Витяг циклу у зовнішню функцію, що створюється в деяких випадках значним (вимірюваним у еталонах) швидкісним покращенням швидкості.
gnasher729

1
Я б сказав про це і сказав, що ОП має бути обережним щодо передчасної оптимізації
Патрік

1
@Patrick Bingo. Якщо ви збираєтеся оптимізувати, скористайтеся профілером, щоб побачити, де знаходяться повільні секції. Не вгадайте. Зазвичай ви можете відчути, де можуть бути повільні секції, але підтвердіть це профілером.
Чендрікс

@ gnasher729 Для вирішення цієї конкретної проблеми знадобиться більше, ніж профайлер - потрібно буде навчитися також читати розібраний машинний код. Поки існує передчасна оптимізація, немає такого поняття, як передчасне навчання (принаймні, у розробці програмного забезпечення).
rwong

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

5

Це питання реалізації компілятора або часу виконання (та його параметрів), і це не можна сказати з певністю.

У межах C та C ++ деякі компілятори будуть вбудовувати виклики на основі налаштувань оптимізації - це можна побачити, вивчивши створену збірку при перегляді таких інструментів, як https://gcc.godbolt.org/

Інші мови, як-от Java, це є частиною часу виконання. Це частина JIT і детально розроблена в цьому питанні . Докладніше розгляньте параметри JVM для HotSpot

-XX:InlineSmallCode=n Вбудовуйте раніше скомпільований метод лише у тому випадку, якщо розмір створеного нативного коду менший за цей. Значення за замовчуванням залежить від платформи, на якій працює JVM.
-XX:MaxInlineSize=35 Максимальний розмір байт-коду методу, який слід вказувати.
-XX:FreqInlineSize=n Максимальний розмір байт-коду часто виконуваного методу, який слід накреслити. Значення за замовчуванням залежить від платформи, на якій працює JVM.

Так, так, компілятор HotSpot JIT вбудує вбудовані методи, що відповідають певним критеріям.

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

Це можна сприймати як хибний підхід, коли CPython не вбудовується, але Jython (Python, що працює в JVM), має деякі вклики. Так само MRI Ruby, не вбудований в той час, як JRuby, і ruby2c, який є транспілятором для рубіну в C ..., який потім може бути вбудованим чи не залежно від параметрів компілятора C, який був складений.

Мови не вбудовані. Реалізація може .


5

Ви шукаєте виступ у неправильному місці. Проблема функціональних викликів не в тому, що вони коштують багато. Є ще одна проблема. Функціональні дзвінки можуть бути абсолютно безкоштовними, і у вас все ще буде ця інша проблема.

Це те, що функція подібна до кредитної картки. Оскільки ви можете легко користуватися ним, ви, як правило, використовуєте його більше, ніж, можливо, слід. Припустимо, ви називаєте це на 20% більше, ніж потрібно. Потім типове велике програмне забезпечення містить кілька шарів, кожен викликує функцій у нижньому шарі, тому коефіцієнт 1,2 може ускладнюватися кількістю шарів. (Наприклад, якщо п’ять шарів, і кожен шар має коефіцієнт уповільнення 1,2, складний коефіцієнт уповільнення становить 1,2 ^ 5 або 2,5.) Це лише один із способів думати про це.

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

ДОДАТО: Невеликий приклад. Одного разу я працював у команді над програмним забезпеченням на заводських поверхах, який відслідковував низку робочих доручень або "робочих місць". Була функція, JobDone(idJob)яка могла визначити, чи виконується робота. Робота виконувалася тоді, коли всі її підзадачі були виконані, і кожне з них було виконано, коли були виконані всі його суб-операції. Усі ці речі відслідковувались у реляційній базі даних. Один виклик до іншої функції міг витягти всю цю інформацію, так JobDoneзвану цю іншу функцію, побачив, чи виконується робота, а решту відкинув. Тоді люди могли легко написати такий код:

while(!JobDone(idJob)){
    ...
}

або

foreach(idJob in jobs){
    if (JobDone(idJob)){
        ...
    }
}

Бачите сенс? Ця функція була настільки "потужною" та легкою для виклику, що вона називалась занадто багато. Тому проблема продуктивності полягала не в тому, щоб входити та виходити з функції. Було, що потрібен більш прямий спосіб сказати, чи були виконані завдання. Знову ж таки, цей код міг бути вбудований у тисячі рядків інакше невинного коду. Спроба виправити це заздалегідь - це те, що намагаються зробити всі, але це як намагатися кидати дротики в темну кімнату. Натомість вам потрібно запустити його, а потім «повільний код» розповісти, що це таке, просто забираючи час. Для цього я використовую випадкові паузи .


1

Я думаю, це дійсно залежить від мови та функції. Хоча компілятори c і c ++ можуть вбудовувати безліч функцій, це не стосується Python або Java.

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

Функції Python - це в основному виконувані об'єкти (а в інфакті ви також можете визначити метод call (), щоб зробити об'єкт об'єкта функцією). Це означає, що на виклик їх досить багато ...

АЛЕ

коли ви визначаєте змінні всередині функцій, інтерпретатор використовує LOADFAST замість звичайної інструкції LOAD у байт-коді, що робить ваш код швидшим ...

Інша справа, що коли ви визначаєте об'єкт, що викликається, можливі такі шаблони, як запам'ятовування, і вони можуть значно прискорити обчислення (за рахунок використання більшої кількості пам'яті). В основному це завжди торгівля. Вартість функціональних викликів також залежить від параметрів, оскільки вони визначають, скільки речей потрібно насправді копіювати в стек (таким чином, в c / c ++ звичайна практика передавати великі параметри, такі як структури, вказівниками / посиланням замість значення).

Я вважаю, що ваше запитання на практиці занадто широке, щоб відповісти повністю на стек-бік.

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

Ви здивуєтеся тому, скільки речей ви дізнаєтесь у цьому процесі.

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

якщо ви задасте більш конкретне запитання, було б легше отримати більш конкретну відповідь, я думаю.


Цитуючи вас: "Я вважаю, що ваше запитання на практиці занадто широке, щоб відповісти повністю на стак-зміні". Як я можу потім його звузити? Я хотів би побачити деякі фактичні дані, що відображають вплив функціональних викликів на продуктивність. Мені байдуже, на якій мові, мені просто цікаво побачити більш детальне пояснення, підкріплене даними, якщо це можливо, як я вже сказав.
dabadaba

Справа в тому, що це залежить від мови. У C і C ++, якщо функція вбудована, вплив дорівнює 0. Якщо не вбудовано, це залежить від його параметрів, чи знаходиться він у кеші чи ні тощо.
ingframin

1

Я вимірював накладні витрати прямих та віртуальних викликів функції C ++ на Xenon PowerPC деякий час тому .

Розглянуті функції мали один параметр і єдине повернення, тому передача параметрів відбувалася в регістри.

Коротше кажучи, накладні витрати прямого (невіртуального) виклику функції склали приблизно 5,5 наносекунд, або 18 тактових циклів, порівняно з вбудованим викликом функції. Накладні витрати віртуального виклику функції склали 13,2 наносекунд, або 42 тактових цикли, порівняно з вбудованими.

Ці таймінги, ймовірно, різні в різних сімействах процесорів. Мій тестовий код тут ; ви можете виконати той же експеримент на вашому обладнанні. Використовуйте високоточний таймер на зразок rdtsc для своєї реалізації CFastTimer; системний час () недостатньо точно.

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