Мої результати були схожі на ваші: код, що використовує проміжні змінні, був досить стабільним щонайменше на 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 разів більше, ніж той, що використовує проміжні змінні.
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
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))
буде виділено 3-й кортеж, і призначення a = tuple(...)
зменшить кількість посилань старого кортежу, змусивши його звільнити, і збільшить лічильник посилань нового кортежу; то те саме відбувається b
. Тому після першої ітерації цих кортежів завжди є принаймні 2, якщо не 3, тож обмолочення не відбувається.
Найголовніше, що не можна гарантувати, що код, що використовує проміжні змінні, завжди швидший - дійсно в деяких установках може бути, що використання проміжних змінних призведе до додаткових mmap
викликів, тоді як код, який безпосередньо порівнює повернені значення, може бути чудовим.
Хтось запитав, чому так відбувається, коли timeit
відключається збір сміття. Це дійсно правда , що timeit
робить це :
Примітка
За замовчуванням timeit()
тимчасово вимикає збір сміття під час синхронізації. Перевага цього підходу полягає в тому, що він робить незалежні терміни більш порівнянними. Цей недолік полягає в тому, що ГХ може бути важливим компонентом роботи вимірюваної функції. Якщо так, то GC можна знову активувати як перший оператор у рядку налаштування. Наприклад:
Однак збирач сміття Python існує лише для того, щоб повернути циклічне сміття , тобто колекції об’єктів, посилання на які утворюють цикли. Тут справа не в цьому; натомість ці об’єкти звільняються негайно, коли кількість посилань падає до нуля.
dis.dis("tuple(range(2000)) == tuple(range(2000))")
зdis.dis("a = tuple(range(2000)); b = tuple(range(2000)); a==b")
. У моїй конфігурації другий фрагмент фактично містить весь байт-код першого та деякі додаткові інструкції. Важко повірити, що більше інструкцій байт-коду призводить до швидшого виконання. Можливо, це якась помилка у конкретній версії Python?