Мои результаты были аналогичны вашим: код с промежуточными переменными довольно стабильно был как минимум на 10-20% быстрее в Python 3.4. Однако, когда я использовал IPython в том же самом интерпретаторе Python 3.4, я получил следующие результаты:
In [1]: %timeit -n10000 -r20 tuple(range(2000)) == tuple(range(2000))
10000 loops, best of 20: 74.2 µs per loop
In [2]: %timeit -n10000 -r20 a = tuple(range(2000)); b = tuple(range(2000)); a==b
10000 loops, best of 20: 75.7 µs per loop
Примечательно, что мне никогда не удавалось приблизиться к 74,2 мкс для первого, когда я использовал -mtimeit
из командной строки.
Так что этот Heisenbug оказался весьма интересным. Я решил запустить команду с strace
и действительно происходит что-то подозрительное:
% strace -o withoutvars python3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 134 usec per loop
% strace -o withvars python3 -mtimeit "a = tuple(range(2000)); b = tuple(range(2000)); a==b"
10000 loops, best of 3: 75.8 usec per loop
% grep mmap withvars|wc -l
46
% grep mmap withoutvars|wc -l
41149
Теперь это хорошая причина для разницы. Код, который не использует переменные, приводит к тому, что системный вызов mmap
вызывается почти в 1000 раз чаще, чем тот, который использует промежуточные переменные.
withoutvars
заполнено mmap
/munmap
для региона 256 КБ; одни и те же строки повторяются снова и снова:
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
mmap(NULL, 262144, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f32e56de000
munmap(0x7f32e56de000, 262144) = 0
Похоже, что вызов mmap
исходит от функции _PyObject_ArenaMmap
из Objects/obmalloc.c
; obmalloc.c
также содержит макрос ARENA_SIZE
, который #define
d должен быть (256 << 10)
(то есть 262144
); аналогично munmap
соответствует _PyObject_ArenaMunmap
из obmalloc.c
.
obmalloc.c
говорит, что
До Python 2.5 арены никогда не были free()
отредактированы. Начиная с Python 2.5, мы пытаемся free()
арены и используем некоторые мягкие эвристические стратегии, чтобы увеличить вероятность того, что арены в конечном итоге могут быть освобождены.
Таким образом, эта эвристика и тот факт, что распределитель объектов Python освобождает эти свободные арены, как только они опустошаются, приводят к python3 -mtimeit 'tuple(range(2000)) == tuple(range(2000))'
запуску патологического поведения, когда одна область памяти размером 256 КБ многократно перераспределяется и освобождается; и это распределение происходит с mmap
/munmap
, что сравнительно дорого, поскольку это системные вызовы — более того, mmap
с MAP_ANONYMOUS
требует, чтобы вновь сопоставленные страницы были обнулены — хотя Python это не заботит.
Поведение отсутствует в коде, который использует промежуточные переменные, потому что он использует немного больше памяти, и никакая область памяти не может быть освобождена, так как некоторые объекты все еще выделены в ней. Это потому, что timeit
превратит его в петлю, мало чем отличающуюся от
for n in range(10000)
a = tuple(range(2000))
b = tuple(range(2000))
a == b
Теперь поведение таково, что и a
, и b
останутся связанными до тех пор, пока они не будут *переназначены, поэтому во второй итерации tuple(range(2000))
выделит третий кортеж, а назначение a = tuple(...)
уменьшит счетчик ссылок старого кортежа, в результате чего он будет выпущено и увеличить количество ссылок нового кортежа; то же самое происходит с b
. Поэтому после первой итерации всегда есть как минимум 2 таких кортежа, если не 3, так что перебора не происходит.
В частности, нельзя гарантировать, что код, использующий промежуточные переменные, всегда будет быстрее — действительно, в некоторых установках может случиться так, что использование промежуточных переменных приведет к дополнительным вызовам mmap
, тогда как код, который напрямую сравнивает возвращаемые значения, может подойти.
Кто-то спросил, почему так происходит, когда timeit
отключает сборку мусора. Действительно, timeit
делает это:
Примечание
По умолчанию timeit()
временно отключает сборку мусора на время. Преимущество этого подхода в том, что он делает независимые тайминги более сопоставимыми. Этот недостаток заключается в том, что GC может быть важным компонентом выполнения измеряемой функции. Если это так, GC можно повторно включить в качестве первого оператора в строке установки. Например:
Однако сборщик мусора Python предназначен только для удаления циклического мусора, т. е. коллекций объектов, ссылки на которые образуют циклы. Здесь это не так; вместо этого эти объекты немедленно освобождаются, когда счетчик ссылок падает до нуля.
person
Antti Haapala
schedule
11.04.2016
146 usec per loop
и148 usec per loop
за 1000 циклов для первой и второй строки соответственно. Они почти одинаковы. - person aluriak   schedule 11.04.2016==
наis
, либо просто используя оператор запятой для создания кортежа с двумя значениями, что уменьшит время (в моем случае) 104 и 92 микросекунды до 68 и 56. Таким образом, это не проблема кэширования памяти при сравнении, а что-то, непосредственно связанное с созданием кортежей. - person Duncan   schedule 11.04.2016dis.dis("tuple(range(2000)) == tuple(range(2000))")
сdis.dis("a = tuple(range(2000)); b = tuple(range(2000)); a==b")
. В моей конфигурации второй фрагмент фактически содержит весь байт-код из первого и некоторые дополнительные инструкции. Трудно поверить, что большее количество инструкций байт-кода приводит к более быстрому выполнению. Может быть, это какая-то ошибка в конкретной версии Python? - person Łukasz Rogalski   schedule 11.04.2016best of 3: …
. Это важно, потому что если возвращаемое значение является лучшим из трех прогонов, то статистически оно ничего не значит для среднего времени. Используя скрипт, использующий timeit для функций, я получаю среднее время 0,17 для обоих. - person aluriak   schedule 11.04.2016"a=b=1; a = tuple(range(2000)); b = tuple(range(2000)); a,b"
68µS,"a = tuple(range(2000)); b = tuple(range(2000)); a,b"
56µS. Также разница становится пропорционально больше, если вы увеличиваете числа до 20 000 или 200 000. - person Duncan   schedule 11.04.2016timeit
напрямую. На сравнение двух отдельных процессов Python может повлиять планировщик задач операционной системы или другие эффекты. - person poke   schedule 11.04.2016