Почему код использует промежуточные переменные быстрее, чем код без них?


я столкнулся с этим странным поведением и не смог объяснить его. Вот эти ориентиры:

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 75

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; the obmalloc.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 потребляется во время процесса синхронизации. Это все равно странно.