Почему [] быстрее, чем 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().


Инструкция BUILD_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:

Здесь первое, что вы видите, когда просматриваете обработку кода 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() ищет вызываемый, а затем вызывает его.

В то время как [] является отображением списка или литералом и, таким образом, позволяет избежать поиска имени и вызова функции.