Почему код использует промежуточные переменные быстрее, чем код без них?
я столкнулся с этим странным поведением и не смог объяснить его. Вот эти ориентиры:
py -3 -m timeit "tuple(range(2000)) == tuple(range(2000))"
10000 loops, best of 3: 97.7 usec per loop
py -3 -m timeit "a = tuple(range(2000)); b = tuple(range(2000)); a==b"
10000 loops, best of 3: 70.7 usec per loop
Как получилось, что сравнение с присвоением переменных происходит быстрее, чем использование одного лайнера с временными переменными более чем на 27%?
по документам Python сборка мусора отключена во время timeit, поэтому это не может быть так. Это какая-то оптимизация?
результаты также могут быть воспроизведены в Python 2.x хотя бы к меньшему степень.
работает под управлением Windows 7, CPython 3.5.1, Intel i7 3.40 GHz, 64 бит как ОС, так и Python. Похоже, что другая машина, которую я пробовал работать на Intel i7 3.60 GHz с Python 3.5.0, не воспроизводит результаты.
запуск с использованием того же процесса Python с timeit.timeit()
@ 10000 петель произвели 0.703 и 0.804 соответственно. По-прежнему показывает, хотя и в меньшей степени. (~12,5%)
2 ответа:
мои результаты были похожи на ваши: код с использованием промежуточных переменных был довольно последовательно, по крайней мере, на 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
из командной строки.так что это плавающая ошибка, оказалось что-то весьма интересное. Я решил бежать команда с
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 раз больше, чем тот, который использует промежуточные переменные.The
withoutvars
полноеmmap
/munmap
для области 256k; эти же строки повторяются снова и снова: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
The
mmap
звонок, кажется, идет из функции_PyObject_ArenaMmap
СObjects/obmalloc.c
; theobmalloc.c
также содержит макросARENA_SIZE
, которая составляет#define
д(256 << 10)
(то есть262144
); аналогичноmunmap
соответствует_PyObject_ArenaMunmap
Сobmalloc.c
.
obmalloc.c
говорит, чтодо Python 2.5 арены никогда не были
free()
' ed. Начиная с Python 2.5, мы стараемсяfree()
арены, и использовать некоторые мягкие эвристические стратегии для увеличения вероятность того, что Арены в конечном итоге могут быть освобожденный.таким образом, эти эвристики и тот факт, что Python object allocator освобождает эти свободные арены, как только они опорожняются, приводят к
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))
выделяем 3-й кортеж, а заданиеa = tuple(...)
уменьшит количество ссылок старого кортежа, что приведет к его освобождению, и увеличит количество ссылок нового кортежа; то же самое происходит сb
. Поэтому после первой итерации всегда есть по крайней мере 2 из этих кортежей, если не 3, поэтому трепка не происходит.прежде всего, нельзя гарантировать, что код, использующий промежуточные переменные, всегда быстрее - действительно, в некоторых настройках может быть, что использование промежуточных переменных переменных приведет к дополнительному
mmap
вызовы, в то время как код, который сравнивает возвращаемые значения напрямую, может быть прекрасным.
кто-то спросил, почему это происходит, когда
timeit
отключает сборку мусора. Это действительно правда, чтоtimeit
это:Примечание
по умолчанию
timeit()
временно отключает сбор мусора во время синхронизации. Преимущество такого подхода заключается в том, что он делает независимые тайминги более сопоставимы. Этот недостаток заключается в том, что ГХ может быть важным компонентом выполнения измеряемой функции. Если это так, GC может быть повторно включен в качестве первого оператора в строке установки. Например:однако сборщик мусора Python существует только для восстановления циклические мусора, т. е. коллекции объектов, ссылки на которые образуют циклы. Здесь это не так; вместо этого эти объекты освобождаются сразу же, когда счетчик ссылок падает до нуля.
первый вопрос здесь должен быть, это воспроизводимо? Для некоторых из нас, по крайней мере, это наверняка, хотя другие люди говорят, что они не видят эффекта. Это на Fedora, с тестом равенства изменен на
is
поскольку на самом деле сравнение не имеет отношения к результату, и диапазон поднялся до 200 000, поскольку это, похоже, максимизирует эффект:$ python3 -m timeit "a = tuple(range(200000)); b = tuple(range(200000)); a is b" 100 loops, best of 3: 7.03 msec per loop $ python3 -m timeit "a = tuple(range(200000)) is tuple(range(200000))" 100 loops, best of 3: 10.2 msec per loop $ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))" 100 loops, best of 3: 10.2 msec per loop $ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))" 100 loops, best of 3: 9.99 msec per loop $ python3 -m timeit "a = b = tuple(range(200000)) is tuple(range(200000))" 100 loops, best of 3: 10.2 msec per loop $ python3 -m timeit "tuple(range(200000)) is tuple(range(200000))" 100 loops, best of 3: 10.1 msec per loop $ python3 -m timeit "a = tuple(range(200000)); b = tuple(range(200000)); a is b" 100 loops, best of 3: 7 msec per loop $ python3 -m timeit "a = tuple(range(200000)); b = tuple(range(200000)); a is b" 100 loops, best of 3: 7.02 msec per loop
я отмечаю, что различия между запусками и порядок, в котором выполняются выражения, имеют очень мало значения к результату.
добавление заданий в
a
иb
в медленной версии не ускоряет его. На самом деле, как и следовало ожидать, назначение локальных переменных имеет незначительный эффект. Единственное, что ускоряет его, - это разделение выражения полностью на два. Единственное различие, которое это должно сделать, заключается в том, что оно уменьшает максимальную глубину стека, используемую Python при оценке выражения (от 4 до 3).это дает нам ключ к тому, что эффект связан с глубина стека, возможно, дополнительный уровень толкает стек на другую страницу памяти. Если это так, мы должны увидеть, что внесение других изменений, влияющих на стек, изменится (скорее всего, убьет эффект), и на самом деле это то, что мы видим:
$ python3 -m timeit -s "def foo(): tuple(range(200000)) is tuple(range(200000))" "foo()" 100 loops, best of 3: 10 msec per loop $ python3 -m timeit -s "def foo(): tuple(range(200000)) is tuple(range(200000))" "foo()" 100 loops, best of 3: 10 msec per loop $ python3 -m timeit -s "def foo(): a = tuple(range(200000)); b = tuple(range(200000)); a is b" "foo()" 100 loops, best of 3: 9.97 msec per loop $ python3 -m timeit -s "def foo(): a = tuple(range(200000)); b = tuple(range(200000)); a is b" "foo()" 100 loops, best of 3: 10 msec per loop
Итак, я думаю, что эффект полностью зависит от того, сколько стека Python потребляется во время процесса синхронизации. Это все равно странно.