Почему из [Module] импорт [Something] занимает больше времени, чем импорт [Module [duplicate]

Я использовал python -mtimeit для тестирования и обнаружил, что from Module import Sth занимает больше времени по сравнению с import Module

E.g.

$ python -mtimeit "import math; math.sqrt(4)"
1000000 loops, best of 3: 0.618 usec per loop
$ python -mtimeit "from math import sqrt; sqrt(4)"
1000000 loops, best of 3: 1.11 usec per loop

то же самое для другого случая. Может ли кто-нибудь объяснить причину? Благодарю вас!


person Mike Lee    schedule 09.08.2013    source источник
comment
Возможно, взгляните на этот пост - stackoverflow.com/a/3592137/1679863   -  person Rohit Jain    schedule 09.08.2013
comment
Я предлагаю не беспокоиться о том, почему это происходит (прошлое интеллектуальное любопытство), и использовать то, что делает ваш код наиболее читабельным.   -  person roippi    schedule 09.08.2013
comment
@RohitJain: мне нравится гипотеза. Однако python -mtimeit "from math import pow, sqrt, sin, fabs, ceil, floor, fmod" занимает только часть больше, чем python -mtimeit "from math import pow", что не поддерживает теорию.   -  person NPE    schedule 09.08.2013


Ответы (2)


Здесь есть две проблемы. Первый шаг — выяснить, какая часть выполняется быстрее: оператор импорта или вызов.

Итак, давайте так:

$ python -mtimeit 'import math'
1000000 loops, best of 3: 0.555 usec per loop
$ python -mtimeit 'from math import sqrt'
1000000 loops, best of 3: 1.22 usec per loop
$ python -mtimeit -s 'from math import sqrt' 'sqrt(10)'
10000000 loops, best of 3: 0.0879 usec per loop
$ python -mtimeit -s 'import math' 'math.sqrt(10)'
10000000 loops, best of 3: 0.122 usec per loop

(Это с 64-разрядной версией Apple CPython 2.7.2 на OS X 10.6.4 на моем ноутбуке. Но python.org 3.4 dev на том же ноутбуке и 3.3.1 на Linux дают примерно одинаковые результаты. С PyPy более интеллектуальное кэширование делает невозможным тестирование, так как все заканчивается за 1 нс… В любом случае, я думаю, что эти результаты, вероятно, настолько портативны, насколько вообще могут быть микротесты.)

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


Концептуально вы можете понять, почему оператор from … import занимает больше времени: у него больше работы. Первая версия должна найти модуль, при необходимости скомпилировать его и выполнить. Вторая версия должна сделать все это, а затем также извлечь sqrt и вставить его в глобальные переменные вашего текущего модуля. Таким образом, он должен быть хотя бы немного медленнее.

Если вы посмотрите на байт-код (например, используя модуль dis и вызвав dis.dis('import math') ), именно в этом разница. Сравнивать:

  0 LOAD_CONST               0 (0) 
  3 LOAD_CONST               1 (None) 
  6 IMPORT_NAME              0 (math) 
  9 STORE_NAME               0 (math) 
 12 LOAD_CONST               1 (None) 
 15 RETURN_VALUE         

… to:

  0 LOAD_CONST               0 (0) 
  3 LOAD_CONST               1 (('sqrt',)) 
  6 IMPORT_NAME              0 (math) 
  9 IMPORT_FROM              1 (sqrt) 
 12 STORE_NAME               1 (sqrt) 
 15 POP_TOP              
 16 LOAD_CONST               2 (None) 
 19 RETURN_VALUE         

Дополнительные манипуляции со стеком (LOAD_CONST и POP_TOP), вероятно, не имеют большого значения, и использование другого аргумента для STORE_NAME вряд ли вообще имеет значение… но IMPORT_FROM — это значительный дополнительный шаг.


Удивительно, но быстрая и грязная попытка профилировать код IMPORT_FROM показывает, что большая часть затрат на самом деле связана с поиском подходящих глобальных переменных для импорта. Я не знаю, почему, но… это означает, что импорт целого ряда имен должен быть ненамного медленнее, чем импорт одного. И, как вы указали в комментарии, это именно то, что вы видите. (Но не придавайте этому слишком большого значения. Есть много причин, по которым IMPORT_FROM может иметь большой постоянный коэффициент и только небольшой линейный, и мы не собираемся набрасывать на него огромное количество имен.)


И последнее: если это действительно имеет значение в реальном коде, и вы хотите получить лучшее из обоих миров, import math; sqrt = math.sqrt быстрее, чем from math import sqrt, но дает такое же небольшое ускорение времени поиска/вызова. (Но опять же, я не могу представить ни одного реального кода, в котором это имело бы значение. Единственный раз, когда вас будет волновать, сколько времени займет sqrt, это когда вы вызываете его миллиард раз, и в этот момент вам уже все равно, сколько времени импорт занимает. Плюс, если вам действительно нужно оптимизировать это, создайте локальную область и привяжите sqrt там, чтобы полностью избежать глобального поиска.)

person abarnert    schedule 09.08.2013
comment
Кто бы ни проголосовал, объясните, почему? - person abarnert; 09.08.2013
comment
лоол, не я, не я - person Mike Lee; 09.08.2013
comment
Для таких людей, как я, плохо знакомых с байт-кодом Python, как вы получили список инструкций? - person seanmcl; 09.08.2013
comment
@SeanMcLaughlin: см. модуль dis. Я обновлю ответ с более подробной информацией. - person abarnert; 09.08.2013

Это не ответ, а некоторая информация. Это требовало форматирования, поэтому я не включил его в комментарий. Вот байт-код для «из математического импорта sqrt»:

>>> from math import sqrt
>>> import dis
>>> def f(n): return sqrt(n)
... 
>>> dis.dis(f)
  1           0 LOAD_GLOBAL              0 (sqrt)
              3 LOAD_FAST                0 (n)
              6 CALL_FUNCTION            1
              9 RETURN_VALUE        

И для «импортной математики»

>>> import math
>>> import dis
>>> dis.dis(math.sqrt)
>>> def f(n): return math.sqrt(n)
... 
>>> dis.dis(f)
  1           0 LOAD_GLOBAL              0 (math)
              3 LOAD_ATTR                1 (sqrt)
              6 LOAD_FAST                0 (n)
              9 CALL_FUNCTION            1
             12 RETURN_VALUE        

Интересно, что у более быстрого метода есть еще одна инструкция.

person seanmcl    schedule 09.08.2013
comment
Действительно ли метод быстрее? Я почти уверен, что оператор импорта на самом деле занимает в два раза больше времени, чем вызов функции. - person abarnert; 09.08.2013
comment
Фактически, из быстрого теста фактическая функция на самом деле медленнее при использовании math.sqrt, 0,122 мкс против 0,0879 мкс. Но оператор import намного быстрее, используя import math, 0,555 мкс против 1,222 мкс. Так что эта информация неактуальна. - person abarnert; 09.08.2013
comment
Как вы отделили время выполнения от времени загрузки? - person seanmcl; 09.08.2013
comment
Я объяснил это в своем ответе. Чтобы получить только время импорта, я вызвал timeit только с операторами импорта. Чтобы получить только время вызова функции, я передал операторы импорта как код установки, а не как часть основного кода (-s из интерфейса командной строки). - person abarnert; 09.08.2013