it-swarm.com.ru

Почему Tuple быстрее, чем список в Python?

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

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

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

53
Vimvq1987

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

$ 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 построение списка из константных литералов примерно одинаково или немного медленнее, чем построение его из значений, на которые ссылаются переменные; но кортежи ведут себя по-разному - построение кортежа из константных литералов обычно в три раза быстрее, чем построение его из значений, на которые ссылаются переменные! Вы можете задаться вопросом, как это может быть, верно? -)

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

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

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

80
Alex Martelli

С помощью модуля 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

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

$ 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

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

16
Alec Thomas

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

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

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

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

16
Duncan

Управляющее резюме

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

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.
 */

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

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

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

$ 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

Здесь как хранится Tuple (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;

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

9
Raymond Hettinger

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

5
Dan Breslau

Одной из областей, где список заметно быстрее, является построение из генератора, и, в частности, понимание списка намного быстрее, чем ближайший эквивалент Tuple, 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] намного быстрее, чем оба.

1
Dan Passaro

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

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

0
y durga prasad