Как работает диспатчеризация байткода внутри VM? Tail call dispatch
(перед прочтением – советую прочитать пост ^ про computed goto)
https://github.com/python/cpython/pull/128718
В
CPython новая оптимизация, которая дает где-то 5% производительности. Я уже рассказывал, что такое computed goto, но теперь есть еще более прикольная и быстрая штука для диспатчеризации байткода.
То есть: вызов следующего опкода в Python коде будет быстрее, а значит – все программы просто бесплатно станут быстрее.
(не путать с
tail call оптимизацией для рекурсии)
Как работает?
Сначала делаем два макроса, которые будут устанавливать нужные атрибуты для компилятора.
Пока только
[[clang::musttail]], про поддержку компиляторов будет ниже. Зачем нужен
preserve_none – можно прочитать тут.
#ifdef Py_TAIL_CALL_INTERP
// Note: [[clang::musttail]] works for GCC 15, but not __attribute__((musttail)) at the moment.
# define Py_MUSTTAIL [[clang::musttail]]
# define Py_PRESERVE_NONE_CC __attribute__((preserve_none))
// Для простоты еще два макроса, просто слишком часто повторяется код:
#define TAIL_CALL_PARAMS _PyInterpreterFrame *frame, _PyStackRef *stack_pointer, PyThreadState *tstate, _Py_CODEUNIT *next_instr, int oparg
#define TAIL_CALL_ARGS frame, stack_pointer, tstate, next_instr, oparg
Далее, создаем новый тип колбеков для "tail-call функций":
Py_PRESERVE_NONE_CC typedef PyObject* (*py_tail_call_funcptr)(TAIL_CALL_PARAMS);
Важный шаг: меняем дефиницию макросов TARGET и DISPATCH_GOTO по аналогии с computed gotos.
Теперь тут будет:
# define TARGET(op) Py_PRESERVE_NONE_CC PyObject *_TAIL_CALL_##op(TAIL_CALL_PARAMS)
# define DISPATCH_GOTO() \
do { \
Py_MUSTTAIL return (INSTRUCTION_TABLE[opcode])(TAIL_CALL_ARGS); \
} while (0)
То есть теперь по факту – все TARGET макросы будут разворачиваться в отдельные функции:
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_BINARY_OP(TAIL_CALL_PARAMS);
В теле такой функции будет очень мало кода – только обработка ее логики. Пример для
BINARY_OP.
Вот они, для каждого опкода:
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_BINARY_OP(TAIL_CALL_PARAMS);
Py_PRESERVE_NONE_CC static PyObject *_TAIL_CALL_LIST_APPEND(TAIL_CALL_PARAMS);
// ...
И мы так же ищем следующий опкод в INSTRUCTION_TABLE[opcode], но теперь мы вызываем функцию, которая там лежит в DISPATCH_GOTO. То есть теперь – у нас теперь есть буквально:
callbacks = {
'BINARY_OP': lambda *args, **kwargs: ...
'LIST_APPEND': lambda *args, **kwargs: ...
}
callbacks[opcode](*args, **kwargs)
И во время конфигурации сборки питона –
проверяем, поддерживает ли наш компилятор такое.
Так почему быстрее?
Теперь – все функции маленькие, их удобно оптимизировать.
Вот тут уточнение из комментов.
Потому что для [[mustail]] не создается дополнительный стекфрейм, asm получается более оптимальным. Я подготовил для вас пример: https://godbolt.org/z/T3Eqnd33e (для таких простых случаев -O2 более чем работает, но все равно)
Для вызова функции foo(int a) было:
mov edi, dword ptr [rbp - 4]
call foo(int)@PLT
add rsp, 16
pop rbp
ret
Стало:
mov edi, dword ptr [rbp - 4]
pop rbp
jmp foo(int)@PLT
call -> jmp!
Статья по теме от автора __attribute__((musttail))
Ограничения
Пока что данное поведение скрыто за флагом --with-tail-call-interp, по-умолчанию в 3.14 оно работать не будет. В следующих версиях – включат по-умолчанию для всех.
Есть еще и техническое ограничение. Пока что такой __attribute__ компилятора поддерживает только clang в llvm>=19 на x86-64 и AArch64. В
следующем релизе gcc, вроде бы, завезут поддержку
Ну и последнее: пока проверили только перформанс с
Profile Guided Optimization (pgo), сколько будет без него – еще не мерили. Сначала вообще заявили прирост на 15%, но потом нашли
баг в llvm, который замедлял код без такой фичи.
Да, у нас тут с вами душный канал, где нет ярких заголовков :(
Обсуждение: чего ждете от 3.14 больше всего?
|
Поддержать |
YouTube |
GitHub |
Чат |