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

13

Извлечение функциональности в методы или функции является обязательным условием модульности кода, читабельности и совместимости, особенно в ООП.

Но это означает, что будет сделано больше вызовов функций.

Как разделение нашего кода на методы или функции в действительности влияет на производительность в современных * языках?

* Самые популярные из них: C, Java, C ++, C #, Python, JavaScript, Ruby ...

dabadaba
источник
1
Я полагаю, что каждая реализация языка, стоящая своей соли, уже несколько десятилетий используется для вставки. IOW, накладные расходы составляют точно 0.
Йорг Миттаг
1
«будет сделано больше вызовов функций», часто это не так, поскольку многие из этих вызовов оптимизируют свои издержки за счет того, что различные компиляторы / интерпретаторы обрабатывают ваш код и встраивают вещи. Если ваш язык не имеет такого рода оптимизаций, я мог бы не считать его современным.
Ixrec
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;
}

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

Вызовы функций (в зависимости от платформы) обычно включают несколько десятков инструкций, в том числе сохранение / восстановление стека. Некоторые вызовы функций состоят из инструкции перехода и возврата.

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

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

CHendrix
источник
1
Я встречался с современными компиляторами (gcc, clang) в ситуациях, когда меня действительно волновало, что они создают довольно плохой код для циклов внутри большой функции . Извлечение цикла в статическую функцию не помогло из-за встраивания. Извлечение цикла во внешнюю функцию создало в некоторых случаях значительное (измеримое в тестах) улучшение скорости.
gnasher729
1
Я бы добавил к этому и сказал бы, что OP должен быть осторожен с преждевременной оптимизацией
Патрик
1
@ Патрик Бинго. Если вы собираетесь оптимизировать, используйте профилировщик, чтобы увидеть, где медленные разделы. Не угадай Обычно вы можете почувствовать, где могут быть медленные участки, но подтвердите это с помощью профилировщика.
Чендрикс
@ gnasher729 Чтобы решить эту конкретную проблему, нужно больше, чем профилировщик - нужно будет научиться читать и разобранный машинный код. Несмотря на преждевременную оптимизацию, преждевременного обучения не существует (по крайней мере, при разработке программного обеспечения).
rwong
У вас может быть эта проблема, если вы вызываете функцию миллион раз, но у вас, скорее всего, будут другие проблемы, которые оказывают значительно большее влияние.
Майкл Шоу
5

Это вопрос реализации компилятора или среды выполнения (и его параметров), и его нельзя сказать с какой-либо определенностью.

В C и C ++ некоторые компиляторы выполняют встроенные вызовы на основе настроек оптимизации - это можно легко увидеть, изучив сгенерированную сборку, если посмотреть на такие инструменты, как https://gcc.godbolt.org/

Другие языки, такие как Java, имеют это как часть среды выполнения. Это является частью JIT и подробно рассматривается в этом вопросе SO . В частности, посмотрите на параметры JVM для HotSpot

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

Так что да, JIT-компилятор HotSpot встроит методы, которые соответствуют определенным критериям.

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

Это можно рассматривать как ошибочный подход: CPython не встроен, но Jython (Python работает в JVM) имеет некоторые встроенные вызовы. Аналогично, MRI Ruby не встроен в то время, как это делает JRuby, и ruby2c, который является транспортером для ruby ​​в C ..., который затем может быть встроенным или нет в зависимости от параметров компилятора C, которые были скомпилированы.

Языки не встроены. Реализации могут .

user227864
источник
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 ++ обычной практикой является передача больших параметров, таких как структуры, по указателям / ссылкам вместо значений).

Я думаю, что ваш вопрос на практике слишком широк, чтобы полностью ответить на stackexchange.

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

Вы будете удивлены, сколько вещей вы узнаете в этом процессе.

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

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

ingframin
источник
Цитирую вас: «Я думаю, что ваш вопрос на практике слишком широкий, чтобы на него полностью ответить на stackexchange». Как я могу сузить это тогда? Я хотел бы видеть некоторые фактические данные, представляющие влияние вызова функции на производительность. Мне все равно, на каком языке, мне просто интересно увидеть более подробное объяснение, подкрепленное данными, если это возможно, как я уже сказал.
Дабадаба
Дело в том, что это зависит от языка. В C и C ++, если функция встроенная, влияние равно 0. Если она не встроенная, это зависит от ее параметров, в кеше она или нет и т. Д.
ingframin
1

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

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

Короче говоря, издержки прямого (не виртуального) вызова функции составляли приблизительно 5,5 наносекунд или 18 тактов по сравнению с вызовом встроенной функции. Издержки при вызове виртуальной функции составили 13,2 наносекунды или 42 такта по сравнению с встроенным.

Эти тайминги, вероятно, различаются для разных семейств процессоров. Мой тестовый код здесь ; Вы можете провести тот же эксперимент на своем оборудовании. Используйте высокоточный таймер, такой как rdtsc, для реализации CFastTimer; системное время () недостаточно точное.

Crashworks
источник