Чому [] швидше, ніж list ()?


706

Нещодавно я порівняв обробку швидкості []і list()і з подивом виявити , що []працює більш ніж в три рази швидше , ніж list(). Я провів той самий тест з, {}і dict()результати були практично однакові: []і {}обидва зайняли близько 0,128 сек / мільйон циклів, в той час як list()і dict()взяли приблизно 0,428 сек / мільйон циклів кожен.

Чому це? Є []і {}(і , ймовірно , ()і ''теж) відразу перейти назад на копії якої - небудь порожній складі литерала , а їх явно Названі аналоги ( list(), dict(), tuple(), str()) повністю йти про створення об'єкта, на насправді чи ні у них є елементи?

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

Я отримав свої результати синхронізації, зателефонувавши timeit.timeit("[]")та timeit.timeit("list()"), і, timeit.timeit("{}")і timeit.timeit("dict()"), порівняти списки та словники відповідно. Я запускаю Python 2.7.9.

Нещодавно я виявив « Чому якщо правда повільніше , ніж якщо 1? » , Яка порівнює продуктивність від if Trueдо if 1і , здається, помацати на аналогічному буквальний проти глобального сценарію; можливо, це варто також розглянути.


2
Зауважте: ()і ''вони особливі, оскільки вони не тільки порожні, вони непорушні, і, таким чином, легко перемогти, щоб зробити їх однотонними; вони навіть не створюють нових об’єктів, просто завантажують сингтон на порожній tuple/ str. Технічно детальна реалізація, але мені важко уявити, чому вони не кешують порожнє tuple/ strз міркувань продуктивності. Так ваша інтуїція про []і {}передаючи назад фондову Літерал був неправий, але це не поширюється на ()і ''.
ShadowRanger

Відповіді:


757

Тому що []і {}є буквальним синтаксисом . 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()є окремими об'єктами. Їх імена потрібно вирішити, стек повинен бути задіяний для висунення аргументів, кадр повинен бути збережений, щоб отримати пізніше, і потрібно здійснити виклик. На це все потрібно більше часу.

Для порожнього випадку це означає, що у вас є щонайменше a LOAD_NAME(який повинен шукати через глобальний простір імен, а також __builtin__модуль ), за яким слідує a 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витрати.


150

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

75

Оскільки listце функція для перетворення рядка, що скаже, в об'єкт списку, в той час як []він використовується для створення списку поза батою. Спробуйте це (може мати більше сенсу для вас):

x = "wham bam"
a = list(x)
>>> a
["w", "h", "a", "m", ...]

Поки

y = ["wham bam"]
>>> y
["wham bam"]

Надає вам фактичний список, що містить все, що ви вклали в нього.


7
Це безпосередньо не стосується питання. Питання полягало у тому, чому []швидше ніж list(), не чому ['wham bam']швидше, ніж list('wham bam').
Джеремі Віссер

2
@JeremyVisser Це мало для мене сенсу, оскільки []/ list()точно таке ж, як ['wham']/ list('wham')тому, що вони мають однакові відмінності в зміні так 1000/10само, як і 100/1в математиці. Теоретично ви могли б забрати, wham bamі факт все одно буде те саме, що list()намагається перетворити щось, викликаючи ім'я функції, а []прямо зараз просто перетворить змінну. Виклики функцій різні, так, це лише логічний огляд проблеми, оскільки, наприклад, мережева карта компанії також є логічною для вирішення / проблеми. Голосуйте, хочете.
Тортується

@JeremyVisser навпаки, це показує, що вони роблять різні операції зі змістом.
Балдрікк

20

Відповіді тут чудові, до речі, і повністю висвітлюють це питання. Я відмовлюся від подальшого кроку від байт-коду для тих, хто цікавиться. Я використовую останнє репо 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це не зрозумілий хлопець, який негайно викликає функцію, він не може. Натомість він захоплює об'єкт із стека, захоплює всі аргументи стеку і потім перемикається на основі типу об'єкта; це:

Ми виклик listтипу, що передається аргумент , щоб call_functionце PyList_Type. Тепер CPython повинен викликати загальну функцію для обробки будь-яких іменованих об'єктів, що називаються _PyObject_FastCallKeywords, та більше викликів функцій.

Ця функція знову робить деякі перевірки для певних типів функцій (які я не можу зрозуміти, чому), а потім, створивши дикт для 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__та ініціалізує список із будь-якими доступними аргументами, тоді ми повертаємось назад, тому, як ми прийшли. :-)

Нарешті, пригадайте LOAD_NAME, це ще один хлопець, який сприяє цьому.


Неважко помітити, що, маючи справу з нашим вкладом, Python, як правило, повинен стрибати через обручі, щоб фактично знайти відповідну Cфункцію, щоб виконати цю роботу. У нього немає нахабності негайно викликати його, оскільки це динамічно, хтось може маскувати list( а хлопці роблять багато людей ), і треба пройти інший шлях.

Тут багато чого list()втрачає: Досліджуючий Python потрібно зробити, щоб з’ясувати, що чорт мусить робити.

Буквальний синтаксис, з іншого боку, означає саме одне; вона не може бути змінена і завжди поводиться заздалегідь визначеним чином.

Виноска: Усі назви функцій можуть змінюватися від одного випуску до іншого. Справа все ще стоїть і, швидше за все, буде стояти в будь-яких майбутніх версіях, динамічний пошук сповільнює роботу.


13

Чому []швидше, ніж 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
>>> a_list = list()
List initialized.
>>> type(a_list)
<class '__main__.List'>

Так само ми могли б видалити його з глобального простору імен

del list

і помістіть його у вбудований простір імен:

import builtins
builtins.list = List

І зараз:

>>> list_0 = list()
List initialized.
>>> type(list_0)
<class '__main__.List'>

І зауважте, що відображення списку створює список беззастережно:

>>> list_1 = []
>>> type(list_1)
<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()шукає дзвінок, а потім викликає його.

В той час [], як відображення списку, або буквальне, і, таким чином, уникає пошуку імені та виклику функції.


2
+1 за вказівку на те, що ви можете викрасти, listі компілятор python не може бути впевнений, чи справді він поверне порожній список.
Beefster
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.