Почему кортеж быстрее, чем список в Python? - PullRequest
59 голосов
/ 27 июля 2010

Я только что прочитал в «Погружение в Python» , что «кортежи быстрее списков».

Кортеж неизменен, а список изменчив, но я не совсем понимаю, почему кортеж быстрее.

Кто-нибудь делал тест производительности на этом?

Ответы [ 7 ]

84 голосов
/ 27 июля 2010

Указанное соотношение «скорость построения» имеет место только для констант кортежей (те, чьи элементы выражены литералами). Внимательно наблюдайте (и повторите на своем компьютере - вам просто нужно набирать команды в командной оболочке / окне команд!) ...:

$ python3.1 -mtimeit -s'x,y,z=1,2,3' '[x,y,z]'
1000000 loops, best of 3: 0.379 usec per loop
$ python3.1 -mtimeit '[1,2,3]'
1000000 loops, best of 3: 0.413 usec per loop

$ python3.1 -mtimeit -s'x,y,z=1,2,3' '(x,y,z)'
10000000 loops, best of 3: 0.174 usec per loop
$ python3.1 -mtimeit '(1,2,3)'
10000000 loops, best of 3: 0.0602 usec per loop

$ python2.6 -mtimeit -s'x,y,z=1,2,3' '[x,y,z]'
1000000 loops, best of 3: 0.352 usec per loop
$ python2.6 -mtimeit '[1,2,3]'
1000000 loops, best of 3: 0.358 usec per loop

$ python2.6 -mtimeit -s'x,y,z=1,2,3' '(x,y,z)'
10000000 loops, best of 3: 0.157 usec per loop
$ python2.6 -mtimeit '(1,2,3)'
10000000 loops, best of 3: 0.0527 usec per loop

Я не проводил измерения на 3.0, потому что, конечно, у меня его нет - он полностью устарел и нет абсолютно никаких причин его хранить, поскольку 3.1 превосходит его во всех отношениях (Python 2.7 Если вы можете выполнить обновление до этого уровня, то есть почти на 20% быстрее, чем 2,6 в каждой задаче - и 2,6, как вы видите, быстрее, чем 3,1 - поэтому, если вы серьезно относитесь к производительности, Python 2.7 действительно единственный Отпусти, что ты должен идти!).

В любом случае, ключевым моментом здесь является то, что в каждом выпуске Python построение списка из константных литералов примерно одинаково или немного медленнее, чем построение его из значений, на которые ссылаются переменные; но кортежи ведут себя по-разному - построение кортежа из константных литералов обычно в три раза быстрее, чем построение его из значений, на которые ссылаются переменные! Вы можете задаться вопросом, как это может быть, верно? -)

Ответ: кортеж, составленный из константных литералов, может легко быть идентифицирован компилятором Python как один, сам неизменяемый константный литерал: так что он по сути создается один раз, когда компилятор превращает источник в байт-коды, и спрятан в «таблица констант» соответствующей функции или модуля. Когда эти байт-коды выполняются, им просто нужно восстановить предварительно созданный постоянный кортеж - эй presto! -)

Эту простую оптимизацию нельзя применить к спискам, поскольку список является изменяемым объектом, поэтому важно, чтобы, если одно и то же выражение, например [1, 2, 3], выполнялось дважды (в цикле - модуль timeit делает цикл от вашего имени ;-), каждый раз заново создается новый новый объект списка - и эта конструкция (например, конструкция кортежа, когда компилятор не может тривиально идентифицировать его как константу времени компиляции и неизменный объект) действительно занимает немного в то время.

Это, как говорится, конструкция кортежа (когда обе конструкции на самом деле должны происходит примерно вдвое быстрее, чем построение списка - и это несоответствие можно объяснить явной простотой кортежа, о которой неоднократно упоминались другие ответы. Но эта простота не учитывает ускорение в шесть и более раз, как вы заметите, если сравнить только списки и кортежи с простыми константными литералами в качестве их элементов! _)

18 голосов
/ 27 июля 2010

Алекс дал отличный ответ, но я попытаюсь остановиться на нескольких вещах, которые, я думаю, стоит упомянуть.Любые различия в производительности, как правило, невелики и зависят от конкретной реализации: поэтому не ставьте на них ферму.

В CPython кортежи хранятся в одном блоке памяти, поэтому создание нового кортежа в худшем случае включает один вызоввыделить память.Списки размещаются в двух блоках: фиксированный со всей информацией об объектах Python и блок переменного размера для данных.Это одна из причин того, почему создание кортежа происходит быстрее, но, вероятно, оно также объясняет небольшую разницу в скорости индексации, поскольку указатель должен следовать на один меньше.

В CPython также есть оптимизации для сокращения выделения памяти:Выделенные объекты списков сохраняются в свободном списке, поэтому их можно использовать повторно, но выделение непустого списка все еще требует выделения памяти для данных.Кортежи сохраняются в 20 свободных списках для кортежей разного размера, поэтому выделение небольшого кортежа часто вообще не потребует каких-либо вызовов выделения памяти.

Подобные оптимизации полезны на практике, но они также могут сделать рискованной зависимостьслишком много о результатах 'timeit' и, конечно, совершенно другое, если вы переходите к чему-то вроде IronPython, где распределение памяти работает совсем по-другому.

16 голосов
/ 27 июля 2010

С помощью модуля timeit вы часто можете самостоятельно решать вопросы, связанные с производительностью:

$ python2.6 -mtimeit -s 'a = tuple(range(10000))' 'for i in a: pass'
10000 loops, best of 3: 189 usec per loop
$ python2.6 -mtimeit -s 'a = list(range(10000))' 'for i in a: pass' 
10000 loops, best of 3: 191 usec per loop

Это показывает, что кортеж ничтожно быстрее, чем список для итерации. Я получаю аналогичные результаты для индексации, но для построения кортеж уничтожает список:

$ python2.6 -mtimeit '(1, 2, 3, 4)'   
10000000 loops, best of 3: 0.0266 usec per loop
$ python2.6 -mtimeit '[1, 2, 3, 4]'
10000000 loops, best of 3: 0.163 usec per loop

Так что, если скорость итерации или индексации являются единственными факторами, разницы практически нет, но для построения кортежи выигрывают.

12 голосов
/ 22 февраля 2018

Резюме

Кортежи имеют тенденцию работать лучше, чем списки почти в каждой категории:

1) Кортежи могут быть постоянно сложенными .

2) Кортежи могут использоваться повторно, а не копироваться.

3) Кортежи компактны и не перераспределяются.

4) Кортежи напрямую ссылаются на свои элементы.

Кортежи могут быть постоянно сложены

Кортежи констант могут быть предварительно рассчитаны с помощью оптимизатора глазка Python или AST-оптимизатора.Списки, с другой стороны, создаются с нуля:

    >>> from dis import dis

    >>> dis(compile("(10, 'abc')", '', 'eval'))
      1           0 LOAD_CONST               2 ((10, 'abc'))
                  3 RETURN_VALUE   

    >>> dis(compile("[10, 'abc']", '', 'eval'))
      1           0 LOAD_CONST               0 (10)
                  3 LOAD_CONST               1 ('abc')
                  6 BUILD_LIST               2
                  9 RETURN_VALUE 

Кортежи не нужно копировать

Запуск tuple(some_tuple) немедленно возвращает себя.Поскольку кортежи являются неизменяемыми, их не нужно копировать:

>>> a = (10, 20, 30)
>>> b = tuple(a)
>>> a is b
True

Для сравнения, list(some_list) требует, чтобы все данные были скопированы в новый список:

>>> a = [10, 20, 30]
>>> b = list(a)
>>> a is b
False

Кортежине перераспределяйте

Поскольку размер кортежа фиксирован, его можно хранить более компактно, чем списки, для которых необходимо перераспределить, чтобы append () эффективно выполнял операции.

Это дает кортежам хорошее космическое преимущество:

>>> import sys
>>> sys.getsizeof(tuple(iter(range(10))))
128
>>> sys.getsizeof(list(iter(range(10))))
200

Вот комментарий от Objects / listobject.c , который объясняет, что делают списки:

/* This over-allocates proportional to the list size, making room
 * for additional growth.  The over-allocation is mild, but is
 * enough to give linear-time amortized behavior over a long
 * sequence of appends() in the presence of a poorly-performing
 * system realloc().
 * The growth pattern is:  0, 4, 8, 16, 25, 35, 46, 58, 72, 88, ...
 * Note: new_allocated won't overflow because the largest possible value
 *       is PY_SSIZE_T_MAX * (9 / 8) + 6 which always fits in a size_t.
 */

Кортежи ссылаются непосредственно на свои элементы

Ссылки на объекты включаются непосредственно в объект кортежа.Напротив, списки имеют дополнительный уровень косвенности к внешнему массиву указателей.

Это дает кортежам небольшое преимущество в скорости для индексированных поисков и распаковки:

$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'a[1]'
10000000 loops, best of 3: 0.0304 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'a[1]'
10000000 loops, best of 3: 0.0309 usec per loop

$ python3.6 -m timeit -s 'a = (10, 20, 30)' 'x, y, z = a'
10000000 loops, best of 3: 0.0249 usec per loop
$ python3.6 -m timeit -s 'a = [10, 20, 30]' 'x, y, z = a'
10000000 loops, best of 3: 0.0251 usec per loop

Здесь - как хранится кортеж (10, 20):

    typedef struct {
        Py_ssize_t ob_refcnt;
        struct _typeobject *ob_type;
        Py_ssize_t ob_size;
        PyObject *ob_item[2];     /* store a pointer to 10 and a pointer to 20 */
    } PyTupleObject;

Здесь - как хранится список [10, 20]:

    PyObject arr[2];              /* store a pointer to 10 and a pointer to 20 */

    typedef struct {
        Py_ssize_t ob_refcnt;
        struct _typeobject *ob_type;
        Py_ssize_t ob_size;
        PyObject **ob_item = arr; /* store a pointer to the two-pointer array */
        Py_ssize_t allocated;
    } PyListObject;

Обратите внимание, чтоОбъект кортежа включает два указателя данных напрямую, в то время как объект списка имеет дополнительный уровень косвенности к внешнему массиву, содержащему два указателя данных.

5 голосов
/ 27 июля 2010

По сути, потому что неизменность кортежа означает, что интерпретатор может использовать для него более гибкую и быструю структуру данных по сравнению со списком.

1 голос
/ 22 декабря 2016

Одна область, где список заметно быстрее, - это построение из генератора, и, в частности, понимание списка намного быстрее, чем ближайший эквивалент кортежа, tuple() с аргументом генератора:

$ python --version
Python 3.6.0rc2
$ python -m timeit 'tuple(x * 2 for x in range(10))'
1000000 loops, best of 3: 1.34 usec per loop
$ python -m timeit 'list(x * 2 for x in range(10))'
1000000 loops, best of 3: 1.41 usec per loop
$ python -m timeit '[x * 2 for x in range(10)]'
1000000 loops, best of 3: 0.864 usec per loop

Обратите внимание, в частности, что tuple(generator) кажется чуть-чуть быстрее, чем list(generator), но [elem for elem in generator] намного быстрее, чем оба.

0 голосов
/ 24 октября 2017

Кортежи идентифицируются компилятором python как одна неизменяемая константа, поэтому компилятор создал только одну запись в хеш-таблице и никогда не менял

Списки являются изменяемыми объектами. Поэтому компилятор обновляет запись, когда мы обновляем список, поэтому он немногонемного медленнее по сравнению с кортежем

...