Почему вызовы функций без аргументов выполняются быстрее?
Я настроил простую пользовательскую функцию, которая принимает некоторые аргументы по умолчанию (Python 3.5):
def foo(a=10, b=20, c=30, d=40):
return a * b + c * d
и синхронизированные различные вызовы к нему с или без указания значений аргументов:
без указания аргументов:
%timeit foo()
The slowest run took 7.83 times longer than the fastest. This could mean that an intermediate result is being cached
1000000 loops, best of 3: 361 ns per loop
аргументы:
%timeit foo(a=10, b=20, c=30, d=40)
The slowest run took 12.83 times longer than the fastest. This could mean that an intermediate result is being cached
1000000 loops, best of 3: 446 ns per loop
как вы можете видеть, есть довольно заметное увеличение времени, необходимого для вызова Аргументы и за не указание их. В простых одноразовых вызовах это может быть незначительным, но накладные расходы масштабируются и становятся более заметными, если выполняется большое количество вызовов функции:
нет аргументов:
%timeit for i in range(10000): foo()
100 loops, best of 3: 3.83 ms per loop
С Аргументами:
%timeit for i in range(10000): foo(a=10, b=20, c=30, d=40)
100 loops, best of 3: 4.68 ms per loop
такое же поведение присутствует и в Python 2.7 где разница во времени между этими вызовами была на самом деле немного больше foo() -> 291ns
и foo(a=10, b=20, c=30, d=40) -> 410ns
почему это случится? Должен ли я вообще пытаться избегать указания значений аргументов во время вызовов?
1 ответов
почему это происходит? Следует избегать указания значений аргументов во время вызова?
Вообще Нет. реальная причина, по которой вы можете видеть это, заключается в том, что функция, которую вы используете просто не вычислительноемких. Таким образом, время, необходимое для дополнительных команд байтового кода, выданных в случае, когда аргументы предоставляются, может быть обнаружено с помощью синхронизации.
если, например, у вас была более интенсивная функция формы:
def foo_intensive(a=10, b=20, c=30, d=40):
[i * j for i in range(a * b) for j in range(c * d)]
это в значительной степени не покажет никакой разницы в требуемом времени:
%timeit foo_intensive()
10 loops, best of 3: 32.7 ms per loop
%timeit foo_intensive(a=10, b=20, c=30, d=40)
10 loops, best of 3: 32.7 ms per loop
даже при масштабировании до большего количества вызовов время, необходимое для выполнения тела функции, просто превосходит небольшие накладные расходы, введенные дополнительными инструкциями байтового кода.
глядя на байтовый код:
один из способов просмотра сгенерированного байтового кода, выданного для каждого случая вызова, - создание функция, которая обертывает foo
и называет это по-разному. А пока давайте создадим fooDefault
для вызовов с использованием аргументов по умолчанию и fooKwargs()
для функций, указывающих аргументы ключевого слова:
# call foo without arguments, using defaults
def fooDefault():
foo()
# call foo with keyword arguments
def fooKw():
foo(a=10, b=20, c=30, d=40)
теперь dis
мы можем видеть различия в байтовом коде между ними. Для версии по умолчанию мы видим, что по существу выдается одна команда (игнорируя POP_TOP
, который присутствует в обоих случаях) для вызова функции, CALL_FUNCTION
:
dis.dis(fooDefaults)
2 0 LOAD_GLOBAL 0 (foo)
3 CALL_FUNCTION 0 (0 positional, 0 keyword pair)
6 POP_TOP
7 LOAD_CONST 0 (None)
10 RETURN_VALUE
С другой стороны, в случае использования слова 8 больше LOAD_CONST
команд для загрузки имен аргументов (a, b, c, d)
и ценностей (10, 20, 30, 40)
в стек значений (даже при загрузке чисел < 256
- это, наверное, очень быстро, в этом случае, так как они кэшируются):
dis.dis(fooKwargs)
2 0 LOAD_GLOBAL 0 (foo)
3 LOAD_CONST 1 ('a') # call starts
6 LOAD_CONST 2 (10)
9 LOAD_CONST 3 ('b')
12 LOAD_CONST 4 (20)
15 LOAD_CONST 5 ('c')
18 LOAD_CONST 6 (30)
21 LOAD_CONST 7 ('d')
24 LOAD_CONST 8 (40)
27 CALL_FUNCTION 1024 (0 positional, 4 keyword pair)
30 POP_TOP # call ends
31 LOAD_CONST 0 (None)
34 RETURN_VALUE
дополнительно, несколько дополнительных шагов вообще необходимы для случая где аргументы ключевых слов не равны нулю. (например, в ceval/_PyEval_EvalCodeWithName()
).
хотя это действительно быстрые команды, они подводят итог. Чем больше аргументов, тем больше сумма и, когда многие вызовы функции фактически выполняются, они накапливаются, чтобы привести к ощутимой разнице во времени выполнения.
прямым результатом этого является то, что чем больше значений мы указываем, тем больше команд должно быть выдано и функция работает медленнее. Кроме того, указание позиционных аргументов, распаковка позиционных аргументов и распаковка аргументов ключевых слов имеют различный объем накладных расходов, связанных с ними:
-
аргументы
foo(10, 20, 30, 40)
: требуются 4 дополнительные команды для загрузки каждого значения. -
список распаковке
foo(*[10, 20, 30, 40])
: 4LOAD_CONST
команды и дополнительныйBUILD_LIST
.- использование списка, как в
foo(*l)
немного сокращает выполнение, так как мы предоставляем уже построенный список, содержащий значения.
- использование списка, как в
-
распаковка словарь
foo(**{'a':10, 'b':20, 'c': 30, 'd': 40})
: 8LOAD_CONST
команды иBUILD_MAP
.- как со списком распаковки
foo(**d)
сократит выполнение, потому что встроенный список будет поставляться.
- как со списком распаковки
всего заказ на выполнение различных случаях звонки:
defaults < positionals < keyword arguments < list unpacking < dictionary unpacking
я предлагаю использовать dis.dis
по этим делам и видя их различия.
в заключение:
как отметил @goofd в комментарии, это действительно то, о чем не следует беспокоиться, это действительно зависит от варианта использования. Если вы часто вызываете "легкие" функции с точки зрения вычислений, указание значений по умолчанию приведет к небольшому увеличению скорости. Если вы часто предоставляете разные значения, это создает рядом с ничего.
таким образом, это, вероятно, незначительно и пытается получить повышение от неясных краевых случаев, как это действительно толкает его. Если вы обнаружите, что делаете это, вы можете посмотреть на такие вещи, как PyPy
и Cython
.