JIT в CPython нужен поток исполняемых инструкций, значит интерпретатор должен уметь записывать всё, что выполняет. Ключевой вопрос в том, сколько за эту возможность платят программы, которым запись не нужна. Кен Джин описал 1 июля путь к решению, попавшему в 3.15.
Первый вариант: два отдельных интерпретатора, обычный и записывающий. Для интерпретатора на хвостовых вызовах это работало приемлемо, а на варианте с computed goto дало около 6% замедления на pyperformance. Одна из причин в том, что код интерпретатора на C фактически удвоился и перестал помещаться в кэш процессора. Второй вариант, флаг режима записи, убирает раздувание кода, но возвращает проверку ветвления в самый горячий путь, на каждую выполняемую инструкцию.
Рабочее решение обходится без проверок вообще. Интерпретатор прыгает к обработчику инструкции через таблицу переходов, и таблиц делают две: адрес нужной лежит в локальной переменной.
🔘 наивная реализация с двумя полноценными таблицами упирается в ту же проблему, это снова два интерпретатора;
🔘 трюк в том, что все записи второй таблицы указывают на одну-единственную инструкцию записи;
🔘 она пишет trace и передаёт управление обычной таблице, получается сужение потока и обратное расширение;
🔘 включение и выключение режима сводится к подмене указателя на таблицу,
ENTER_TRACING() и LEAVE_TRACING();🔘 в игрушечном замере медиана по 40 запускам составила 1,72 микросекунды без записи и 7,47 микросекунды с записью и JIT.
Автор оценивает накладные расходы максимум в 4,5x и сразу оговаривает, что сравнивать это с накладными расходами 900–1000x у PyPy некорректно.
Чем профилируете горячий Python-код сейчас и во сколько раз он от этого замедляется?
@zen_of_python