Почему [] быстрее, чем list()?
Недавно я сравнил скорость обработки []
и list()
и с удивлением обнаружил, что []
работает более чем в три раза быстрее, чем list()
. Я провел те же тесты с {}
и dict()
, и результаты были практически идентичными: []
и {}
оба заняли около 0,128 сек/миллион циклов, а list()
и dict()
заняли примерно 0,488 секунды/миллион каждый цикл.
Почему это? Делайте []
и {}
(и, возможно, ()
и ''
), немедленно передавайте копии некоторого пустого литерала, в то время как их явно названные экземпляры (list()
, dict()
, tuple()
, str()
) полностью идет о создании объекта, действительно ли у них есть элементы?
Я не знаю, как эти два метода отличаются, но я хотел бы узнать.
Я не мог найти ответ в документах или на SO, и поиск пустых скобок оказался более проблематичным, чем я ожидал.
Я получил результаты синхронизации, вызвав timeit.timeit("[]")
и timeit.timeit("list()")
, и timeit.timeit("{}")
и timeit.timeit("dict()")
, чтобы сравнить списки и словари соответственно. Я запускаю Python 2.7.9.
Недавно я обнаружил, что "Почему это правда медленнее, чем если 1?", который сравнивает производительность if True
с if 1
и, похоже, затрагивает аналогичные литеральный или глобальный сценарий; возможно, это стоит рассмотреть также.
Ответы
Ответ 1
Потому что []
и {}
- это литеральный синтаксис. Python может создавать байт-код только для создания объектов списка или словаря:
>>> import dis
>>> dis.dis(compile('[]', '', 'eval'))
1 0 BUILD_LIST 0
3 RETURN_VALUE
>>> dis.dis(compile('{}', '', 'eval'))
1 0 BUILD_MAP 0
3 RETURN_VALUE
list()
и dict()
являются отдельными объектами. Их имена должны быть разрешены, стек должен быть задействован для ввода аргументов, кадр должен быть сохранен для последующего извлечения, и нужно сделать вызов. Для этого требуется больше времени.
Для пустого случая это означает, что у вас есть как минимум LOAD_NAME
(который также должен искать в глобальном пространстве имен как __builtin__
module), а затем CALL_FUNCTION
, который должен сохранить текущий кадр:
>>> dis.dis(compile('list()', '', 'eval'))
1 0 LOAD_NAME 0 (list)
3 CALL_FUNCTION 0
6 RETURN_VALUE
>>> dis.dis(compile('dict()', '', 'eval'))
1 0 LOAD_NAME 0 (dict)
3 CALL_FUNCTION 0
6 RETURN_VALUE
Время поиска имени можно разделить с помощью timeit
:
>>> import timeit
>>> timeit.timeit('list', number=10**7)
0.30749011039733887
>>> timeit.timeit('dict', number=10**7)
0.4215109348297119
Временное несоответствие, вероятно, связано с хеш-столкновением словаря. Вычтите те времена из времени для вызова этих объектов и сравните результат со временем использования литералов:
>>> timeit.timeit('[]', number=10**7)
0.30478692054748535
>>> timeit.timeit('{}', number=10**7)
0.31482696533203125
>>> timeit.timeit('list()', number=10**7)
0.9991960525512695
>>> timeit.timeit('dict()', number=10**7)
1.0200958251953125
Таким образом, для вызова объекта требуется еще 1.00 - 0.31 - 0.30 == 0.39
секунд на 10 миллионов вызовов.
Вы можете избежать глобальной стоимости поиска, наложив глобальные имена в качестве локальных пользователей (используя установку timeit
, все, что вы связываете с именем, является локальным):
>>> timeit.timeit('_list', '_list = list', number=10**7)
0.1866450309753418
>>> timeit.timeit('_dict', '_dict = dict', number=10**7)
0.19016098976135254
>>> timeit.timeit('_list()', '_list = list', number=10**7)
0.841480016708374
>>> timeit.timeit('_dict()', '_dict = dict', number=10**7)
0.7233691215515137
но вы никогда не сможете преодолеть эту стоимость CALL_FUNCTION
.
Ответ 2
list()
требуется глобальный поиск и вызов функции, но []
скомпилируется в одну команду. См:
Python 2.7.3
>>> import dis
>>> print dis.dis(lambda: list())
1 0 LOAD_GLOBAL 0 (list)
3 CALL_FUNCTION 0
6 RETURN_VALUE
None
>>> print dis.dis(lambda: [])
1 0 BUILD_LIST 0
3 RETURN_VALUE
None
Ответ 3
Потому что list
является function, чтобы преобразовать строку в объект списка, а []
используется для создания списка с места в карьер. Попробуйте это (может иметь для вас больше смысла):
x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]
В то время как
y = ["wham bam"]
>>> y
["wham bam"]
Дает вам фактический список, содержащий все, что вы в него вложили.
Ответ 4
Ответы здесь замечательные, по существу и полностью охватывают этот вопрос. Я сниму еще один шаг от байт-кода для тех, кого это интересует. Я использую последнее репо CPython; более старые версии ведут себя одинаково в этом отношении, но могут быть небольшие изменения.
Здесь прорыв выполнения для каждого из них, BUILD_LIST
для []
и CALL_FUNCTION
для list()
.
Вы должны просто просмотреть ужас:
PyObject *list = PyList_New(oparg);
if (list == NULL)
goto error;
while (--oparg >= 0) {
PyObject *item = POP();
PyList_SET_ITEM(list, oparg, item);
}
PUSH(list);
DISPATCH();
Ужасно запутанный, я знаю. Вот как это просто:
- Создайте новый список с
PyList_New
(это в основном выделяет память для нового объекта списка), oparg
, сигнализируя о количестве аргументы в стеке. Прямо к точке.
- Убедитесь, что с
if (list==NULL)
ничего не получилось.
- Добавьте любые аргументы (в нашем случае это не выполняется), расположенные в стеке с
PyList_SET_ITEM
(макрос).
Неудивительно, что это быстро! Он создан для создания новых списков, ничего другого: -)
Здесь первое, что вы видите, когда просматриваете обработку кода CALL_FUNCTION
:
PyObject **sp, *res;
sp = stack_pointer;
res = call_function(&sp, oparg, NULL);
stack_pointer = sp;
PUSH(res);
if (res == NULL) {
goto error;
}
DISPATCH();
Выглядит довольно безобидно, не так ли? Ну, нет, к сожалению нет, CALL_FUNCTION
- это не простой парень, который сразу вызовет эту функцию, он не сможет. Вместо этого он захватывает объект из стека, захватывает все аргументы стека и затем переключается в зависимости от типа объекта; это a:
Мы вызываем тип list
, аргумент, переданный в CALL_FUNCTION
, PyList_Type
. CPython теперь должен вызывать общую функцию для обработки любых вызываемых объектов с именем _PyObject_FastCallKeywords
, yay больше вызовов функций.
Эта функция снова выполняет некоторые проверки для определенных типов функций (которые я не могу понять почему), а затем после создания dict для kwargs, если требуется, продолжает вызывать _PyObject_FastCallDict
.
_PyObject_FastCallDict
наконец-то нас достает! После выполнения еще больших проверок он захватывает tp_call
слот из type
type
, который мы передали, он захватывает type.tp_call
. Затем он начинает создавать кортеж из аргументов, переданных с помощью _PyStack_AsTuple
и, наконец, наконец, можно сделать вызов!
tp_call
, который соответствует type.__call__
, берет на себя и, наконец, создает объект списка. Он вызывает списки __new__
, которые соответствуют PyType_GenericNew
и выделяет для него память PyType_GenericAlloc
: На самом деле это часть, в которой он догоняет PyList_New
, наконец. Все предыдущие необходимы для обработки объектов общим способом.
В конце концов, type_call
вызывает list.__init__
и инициализирует список любыми доступными аргументами, затем мы возвращаемся обратно, как мы пришли.: -)
Наконец, remmeber LOAD_NAME
, который добавляет другой парень.
Легко видеть, что, когда мы имеем дело с нашим входом, Python обычно должен прыгать через обручи, чтобы действительно найти подходящую функцию C
для выполнения задания. У него нет реплики сразу же вызвать его, потому что он динамичен, кто-то может маскировать list
(и мальчик делает много людей), и должен быть принят другой путь.
Здесь list()
теряет много: исследовать Python нужно, чтобы выяснить, что он должен делать.
Литеральный синтаксис, с другой стороны, означает ровно одну вещь; он не может быть изменен и всегда ведет себя определенным образом.
Сноска: все имена функций могут быть изменены с одной версии на другую. Эта точка по-прежнему стоит и, скорее всего, будет стоять в любых будущих версиях, это динамический поиск, замедляющий работу.
Ответ 5
Почему []
быстрее, чем list()
?
Самая большая причина в том, что Python рассматривает list()
точно так же, как пользовательская функция, что означает, что вы можете перехватить ее, наложив что-то еще на list
и сделайте что-то другое (например, используйте свой собственный список подкласса или, возможно,).
Он немедленно создает список с []
.
Мои объяснения направлены на то, чтобы дать вам интуицию для этого.
Объяснение
[]
широко известен как буквенный синтаксис.
В грамматике это называется "отображением списка". Из документов:
Отображение списка - это, возможно, пустая серия выражений, заключенная в квадратные скобки:
list_display ::= "[" [starred_list | comprehension] "]"
Отображение списка дает новый объект списка, указанное содержимое либо списком выражений, либо пониманием. Когда представлен список выражений, разделенных запятыми, его элементы оценивается слева направо и помещается в объект списка в этом заказ. Когда предоставляется понимание, список создается из элементы, возникающие в результате понимания.
Короче говоря, это означает, что создается встроенный объект типа list
.
Об этом не обходится - это означает, что Python может делать это как можно быстрее.
С другой стороны, list()
можно перехватить из создания встроенного list
с помощью встроенного конструктора списков.
Например, скажем, мы хотим, чтобы наши списки создавались шумно:
class List(list):
def __init__(self, iterable=None):
if iterable is None:
super().__init__()
else:
super().__init__(iterable)
print('List initialized.')
Затем мы могли бы перехватить имя list
на глобальной области уровня модуля, а затем, когда мы создадим list
, мы фактически создаем наш подтипированный список:
>>> list = List
>>> l = list()
List initialized.
>>> type(l)
<class '__main__.List'>
Аналогично, мы могли бы удалить его из глобального пространства имен
del list
и поместите его во встроенное пространство имен:
import builtins
builtins.list = List
И теперь:
>>> l0 = list()
List initialized.
>>> type(l0)
<class '__main__.List'>
И обратите внимание, что отображение списка создает список безоговорочно:
>>> l1 = []
>>> type(l1)
<class 'list'>
Мы, вероятно, делаем это временно, поэтому отменим наши изменения - сначала удалим новый объект list
из встроенных функций:
>>> del builtins.list
>>> builtins.list
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: module 'builtins' has no attribute 'list'
>>> list()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'list' is not defined
О, нет, мы проиграли оригинал.
Не волнуйтесь, мы все равно можем получить list
- это тип литерала списка:
>>> builtins.list = type([])
>>> list()
[]
Итак...
Почему []
быстрее, чем list()
?
Как мы видели - мы можем перезаписать list
- но мы не можем перехватить создание типа литерала. Когда мы используем list
, нам нужно сделать поиск, чтобы узнать, есть ли что-нибудь.
Затем мы должны вызывать все вызываемые, которые мы подняли. Из грамматики:
Вызов вызывает вызываемый объект (например, функцию) с возможным пустой ряд аргументов:
call ::= primary "(" [argument_list [","] | comprehension] ")"
Мы видим, что он делает то же самое для любого имени, а не только для списка:
>>> import dis
>>> dis.dis('list()')
1 0 LOAD_NAME 0 (list)
2 CALL_FUNCTION 0
4 RETURN_VALUE
>>> dis.dis('doesnotexist()')
1 0 LOAD_NAME 0 (doesnotexist)
2 CALL_FUNCTION 0
4 RETURN_VALUE
Для []
нет вызова функции на уровне байт-кода Python:
>>> dis.dis('[]')
1 0 BUILD_LIST 0
2 RETURN_VALUE
Он просто подходит для создания списка без каких-либо поисков или вызовов на уровне байт-кода.
Заключение
Мы продемонстрировали, что list
может быть перехвачен кодом пользователя с использованием правил определения области видимости и что list()
ищет вызываемый, а затем вызывает его.
В то время как []
является отображением списка или литералом и, таким образом, позволяет избежать поиска имени и вызова функции.