Python 3.11 оптимизирован хуже, чем 3.10?
Я запускаю этот простой цикл с Python 3.10.7 и 3.11.0 в Windows 10.
import time
a = 'a'
start = time.time()
for _ in range(1000000):
a += 'a'
end = time.time()
print(a[:5], (end-start) * 1000)
Старая версия выполняется за 187 мс, Python 3.11 требует около 17000 мс. Понимает ли 3.10, что только первые 5 символов
2 ответа
TL;DR: вы не должны использовать такой цикл в любом критически важном для производительности коде, но
Общие рекомендации
Это антипаттерн . Вы не должны писать такой код, если хотите, чтобы это было быстро. Это описано в PEP-8:
Код должен быть написан таким образом, чтобы не мешать другим реализациям Python (PyPy, Jython, IronPython, Cython, Psyco и т. д.).
Например, не полагайтесь на эффективную реализацию CPython конкатенации строк на месте для операторов в формеили . Эта оптимизация ненадежна даже в CPython (она работает только для некоторых типов) и вообще отсутствует в реализациях, не использующих подсчет ссылок. В частях библиотеки, чувствительных к производительности,a = a + b
Вместо этого следует использовать форму . Это гарантирует, что конкатенация будет происходить за линейное время в различных реализациях.
Действительно, другие реализации, такие как PyPy, например, не выполняют эффективную конкатенацию строк на месте. Для каждой итерации создается новая строка большего размера (поскольку строки неизменяемы, на предыдущую можно ссылаться, а PyPy использует не подсчет ссылок, а сборщик мусора). Это приводит к квадратичному времени выполнения, в отличие от линейного времени выполнения в CPython (по крайней мере, в прошлой реализации).
Глубокий анализ
Я могу воспроизвести проблему в Windows 10 между встроенной (64-разрядной x86-64) версией CPython 3.10.8 и версией 3.11.0:
Timings:
- CPython 3.10.8: 146.4 ms
- CPython 3.11.0: 15186.8 ms
Оказывается, код особо не изменился между CPython 3.10 и 3.11, когда дело доходит до добавления строки Unicode. См. например: 3.10 и 3.11 .
Низкоуровневый анализ профилирования показывает, что почти все время тратится на вызов одной безымянной функции другой безымянной функции, вызываемой (которая также остается неизменной между CPython 3.10.8 и 3.11.0). Эта медленная безымянная функция содержит довольно небольшой набор ассемблерных инструкций и почти все время тратится на одну уникальную ассемблерную инструкцию x86-64:
Дело в том, что вызов, безусловно, является корнем проблемы с производительностью, и я думаю, что CPython 3.10.8 быстрее, потому что он, безусловно, вызывает вместо этого. Оба вызова выполняются непосредственно основным большим циклом оценки интерпретатора, и этот цикл управляется сгенерированным байт-кодом.
Оказывается, сгенерированный байт-код различается между двумя версиями, и это является корнем проблемы с производительностью . Действительно, CPython 3.10 генерирует
CPython 3.10 loop:
>> 28 FOR_ITER 6 (to 42)
30 STORE_NAME 4 (_)
6 32 LOAD_NAME 1 (a)
34 LOAD_CONST 2 ('a')
36 INPLACE_ADD <----------
38 STORE_NAME 1 (a)
40 JUMP_ABSOLUTE 14 (to 28)
CPython 3.11 loop:
>> 66 FOR_ITER 7 (to 82)
68 STORE_NAME 4 (_)
6 70 LOAD_NAME 1 (a)
72 LOAD_CONST 2 ('a')
74 BINARY_OP 13 (+=) <----------
78 STORE_NAME 1 (a)
80 JUMP_BACKWARD 8 (to 66)
Эти изменения , кажется , исходят из этой проблемы . Код основного цикла интерпретатора (см. ceval.c) различается в двух версиях CPython. Вот код, выполняемый двумя версиями:
// In CPython 3.10.8
case TARGET(INPLACE_ADD): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *sum;
if (PyUnicode_CheckExact(left) && PyUnicode_CheckExact(right)) {
sum = unicode_concatenate(tstate, left, right, f, next_instr); // <-----
/* unicode_concatenate consumed the ref to left */
}
else {
sum = PyNumber_InPlaceAdd(left, right);
Py_DECREF(left);
}
Py_DECREF(right);
SET_TOP(sum);
if (sum == NULL)
goto error;
DISPATCH();
}
//----------------------------------------------------------------------------
// In CPython 3.11.0
TARGET(BINARY_OP_ADD_UNICODE) {
assert(cframe.use_tracing == 0);
PyObject *left = SECOND();
PyObject *right = TOP();
DEOPT_IF(!PyUnicode_CheckExact(left), BINARY_OP);
DEOPT_IF(Py_TYPE(right) != Py_TYPE(left), BINARY_OP);
STAT_INC(BINARY_OP, hit);
PyObject *res = PyUnicode_Concat(left, right); // <-----
STACK_SHRINK(1);
SET_TOP(res);
_Py_DECREF_SPECIALIZED(left, _PyUnicode_ExactDealloc);
_Py_DECREF_SPECIALIZED(right, _PyUnicode_ExactDealloc);
if (TOP() == NULL) {
goto error;
}
JUMPBY(INLINE_CACHE_ENTRIES_BINARY_OP);
DISPATCH();
}
Обратите внимание, что
Люди в комментариях сообщили, что у них нет проблем с производительностью в Linux. Однако экспериментальные испытания показывают
Обновление: к исправлению
Я открыл вопрос об этом, доступный здесь . Не следует, что размещение кода в функции происходит значительно быстрее из-за того, что переменная является локальной (как указано @Dennis в комментариях).
Похожие сообщения:
Как упоминалось в другом ответе, это действительно регресс, но он будет исправлен в Python 3.12 из проблемы GitHub:
FTR линейное поведение времени будет восстановлено для глобальных (и нелокальных) в 3.12 как побочный эффект регистра VM.