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

Я только что прочитал "Dive into Python" , что "кортежи быстрее, чем списки".

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

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

Ответ 1

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

$ 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 как единый неизменный константный литерал: он по существу строится только один раз, когда компилятор превращает источник в байт-коды и спрятан в "таблица констант" соответствующей функции или модуля. Когда эти байт-коды выполняются, им просто нужно восстановить предварительно построенный постоянный кортеж - hey presto! -)

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

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

Ответ 2

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

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

Ответ 3

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

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

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

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

Ответ 4

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

Ответ 5

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

Ответ 6

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

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