Почему код использует промежуточные переменные быстрее, чем код без них?
я столкнулся с этим странным поведением и не смог объяснить его. Вот эти ориентиры:
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 потребляется во время процесса синхронизации. Это все равно странно.