Cinder — это внутренняя производственная версия CPython 3.10, ориентированная на производительность компании Meta. Он содержит ряд оптимизаций производительности, в том числе встроенное кэширование байт-кода, быструю оценку сопрограмм, поэтапную JIT-компиляцию и экспериментальный компилятор байт-кода, который использует аннотации типов для создания специализированного по типу байт-кода, который лучше работает в JIT.
Cinder лежит в основе Instagram, где он зародился, и все чаще используется во все большем количестве приложений Python в Meta.
Дополнительную информацию о CPython см. в README.cpython.rst
.
Короткий ответ: нет.
Мы сделали Cinder общедоступным, чтобы облегчить обсуждение возможной передачи части этой работы в CPython и уменьшить дублирование усилий среди людей, работающих над производительностью CPython.
Cinder не дорабатывается и не документируется для чьего-либо использования. У нас нет желания, чтобы он стал альтернативой CPython. Наша цель сделать этот код доступным — сделать унифицированный и более быстрый CPython. Таким образом, хотя мы и запускаем Cinder в производство, если вы решите это сделать, вы будете предоставлены сами себе. Мы не можем брать на себя обязательство исправлять внешние отчеты об ошибках или проверять запросы на включение. Мы следим за тем, чтобы Cinder был достаточно стабильным и быстрым для наших производственных рабочих нагрузок, но мы не даем никаких гарантий относительно его стабильности, правильности или производительности для любых внешних рабочих нагрузок или вариантов использования.
Тем не менее, если у вас есть опыт работы с динамическими языковыми средами выполнения и есть идеи, как сделать Cinder быстрее; или если вы работаете над CPython и хотите использовать Cinder в качестве вдохновения для улучшений в CPython (или помочь перейти от частей Cinder к CPython), свяжитесь с нами; мы хотели бы пообщаться!
Cinder должен создаваться так же, как CPython; configure
и make -j
. Однако, поскольку большая часть разработки и использования Cinder происходит в весьма специфическом контексте Meta, мы не особо применяем его в других средах. Таким образом, наиболее надежный способ сборки и запуска Cinder — это повторное использование настройки на основе Docker из нашего рабочего процесса GitHub CI.
Если вы просто хотите получить работающий Cinder, не создавая его самостоятельно, наш образ Docker для выполнения во время выполнения будет самым простым (клон репо не нужен!):
docker run -it --rm ghcr.io/facebookincubator/cinder-runtime:cinder-3.10
Если вы хотите построить его самостоятельно:
git clone https://github.com/facebookincubator/cinder
docker run -v "$PWD/cinder:/vol" -w /vol -it --rm ghcr.io/facebookincubator/cinder/python-build-env:latest bash
./configure && make
Имейте в виду, что Cinder создан или протестирован только на Linux x64; что-нибудь еще (включая macOS), вероятно, не будет работать. Приведенный выше образ Docker основан на Fedora Linux и создан на основе файла спецификации Docker в репозитории Cinder: .github/workflows/python-build-env/Dockerfile
.
Есть несколько новых тестовых целей, которые могут быть интересны. make testcinder
во многом аналогичен make test
за исключением того, что он пропускает несколько тестов, которые являются проблематичными в нашей среде разработки. make testcinder_jit
запускает набор тестов с полностью включенным JIT, поэтому все функции выполняются в формате JIT. make testruntime
запускает набор модульных тестов C++ gtest для JIT. И make test_strict_module
запускает набор тестов для строгих модулей (см. ниже).
Обратите внимание, что эти шаги создают двоичный файл Cinder Python без включенной оптимизации PGO/LTO, поэтому не ждите, что с помощью этих инструкций вы получите какое-либо ускорение при любой рабочей нагрузке Python.
Cinder Explorer — это живая игровая площадка, где вы можете увидеть, как Cinder компилирует код Python из исходного кода в сборку. Приглашаем вас попробовать! Не стесняйтесь отправлять запросы на добавление функций и отчеты об ошибках. Имейте в виду, что Cinder Explorer, как и все остальное, «поддерживается» по мере возможности.
Instagram использует многопроцессную архитектуру веб-сервера; родительский процесс запускается, выполняет работу по инициализации (например, загрузку кода) и разветвляет десятки рабочих процессов для обработки клиентских запросов. Рабочие процессы периодически перезапускаются по ряду причин (например, утечки памяти, развертывание кода) и имеют относительно короткое время жизни. В этой модели ОС должна скопировать всю страницу, содержащую объект, который был выделен в родительском процессе, когда счетчик ссылок объекта изменяется. На практике объекты, выделенные в родительском процессе, живут дольше рабочих; вся работа, связанная с подсчетом ссылок, для них ненужна.
Instagram имеет очень большую кодовую базу Python, и накладные расходы, связанные с копированием при записи долгоживущих объектов с подсчетом ссылок, оказались значительными. Мы разработали решение под названием «бессмертные экземпляры», позволяющее исключить объекты из подсчета ссылок. Подробности см. в разделе Include/object.h. Эта функция контролируется определением Py_IMMORTAL_INSTANCES и включена в Cinder по умолчанию. Это был большой выигрыш для нас в производстве (~5%), но прямой код замедляется. Операции подсчета ссылок происходят часто и должны проверять, участвует ли объект в подсчете ссылок, когда эта функция включена.
«Теневой код» или «теневой байт-код» — это наша реализация специализированного интерпретатора. Он наблюдает за конкретными оптимизируемыми случаями выполнения общих кодов операций Python и (для горячих функций) динамически заменяет эти коды операций специализированными версиями. Ядро теневого кода находится в Shadowcode/shadowcode.c
, хотя реализации специализированных байт-кодов находятся в Python/ceval.c
вместе с остальной частью цикла eval. Тесты, специфичные для теневого кода, находятся в Lib/test/test_shadowcode.py
.
По духу он похож на специализированный адаптивный интерпретатор (PEP-659), который будет встроен в CPython 3.11.
Сервер Instagram представляет собой большую асинхронную рабочую нагрузку, где каждый веб-запрос может запускать сотни тысяч асинхронных задач, многие из которых могут быть выполнены без приостановки (например, благодаря запоминаемым значениям).
Мы расширили протокол векторного вызова, добавив в него новый флаг Ci_Py_AWAITED_CALL_MARKER
, указывающий, что вызывающая сторона немедленно ожидает этого вызова.
При использовании с вызовами асинхронных функций, которые ожидаются немедленно, мы можем немедленно (с нетерпением) оценить вызываемую функцию, вплоть до ее завершения или до ее первой приостановки. Если функция завершается без приостановки, мы можем вернуть значение немедленно, без дополнительного выделения кучи.
При использовании с асинхронным сбором мы можем немедленно (быстро) оценить набор переданных ожидаемых объектов, потенциально избегая затрат на создание и планирование нескольких задач для сопрограмм, которые могут выполняться синхронно, завершенных фьючерсов, запоминаемых значений и т. д.
Эти оптимизации привели к значительному (~5%) повышению эффективности процессора.
В основном это реализовано в Python/ceval.c
с помощью нового флага векторного вызова Ci_Py_AWAITED_CALL_MARKER
, указывающего, что вызывающая сторона немедленно ожидает этого вызова. Найдите использование макроса IS_AWAITED()
и этого флага векторного вызова.
Cinder JIT — это индивидуальный JIT, реализуемый на C++. Он включается с помощью флага -X jit
или переменной среды PYTHONJIT=1
. Он поддерживает почти все коды операций Python и может повысить скорость в 1,5–4 раза во многих тестах производительности Python.
По умолчанию, если этот параметр включен, он будет JIT-компилировать каждую когда-либо вызываемую функцию, что вполне может сделать вашу программу медленнее, а не быстрее из-за накладных расходов на JIT-компиляцию редко вызываемых функций. Опция -X jit-list-file=/path/to/jitlist.txt
или PYTHONJITLISTFILE=/path/to/jitlist.txt
может указывать на текстовый файл, содержащий полные имена функций (в форме path.to.module:funcname
или path.to.module:ClassName.method_name
), по одному на строку, которая должна быть скомпилирована JIT. Мы используем эту опцию для компиляции только набора горячих функций, полученных на основе данных профилирования производства. (Более типичным подходом для JIT была бы динамическая компиляция функций по мере того, как они часто вызываются. Нам пока не стоило реализовывать это, поскольку наша производственная архитектура представляет собой веб-сервер до форка, и для По причинам совместного использования памяти мы хотим выполнить всю нашу JIT-компиляцию заранее в начальном процессе до того, как будут созданы рабочие процессы, что означает, что мы не можем наблюдать за рабочей нагрузкой в процессе, прежде чем решить, какие функции JIT-компилировать.)
JIT находится в каталоге Jit/
, а его тесты C++ — в RuntimeTests/
(запускайте их с помощью make testruntime
). Для этого также есть несколько тестов Python в Lib/test/test_cinderjit.py
; они не претендуют на исчерпывающий характер, поскольку мы запускаем весь набор тестов CPython в JIT-компиляции с помощью make testcinder_jit
; они охватывают крайние случаи JIT, которые иначе не встречаются в наборе тестов CPython.
См. Jit/pyjit.cpp
для получения информации о некоторых других параметрах -X
и переменных среды, которые влияют на поведение JIT. В этом файле также определен модуль cinderjit
, который предоставляет некоторые утилиты JIT для кода Python (например, принудительная компиляция определенной функции, проверка скомпилированности функции, отключение JIT). Обратите внимание, что cinderjit.disable()
отключает только будущую компиляцию; он немедленно компилирует все известные функции и сохраняет существующие функции, скомпилированные JIT.
JIT сначала понижает байт-код Python до промежуточного представления высокого уровня (HIR); это реализовано в Jit/hir/
. HIR достаточно близко сопоставляется с байт-кодом Python, хотя это регистровая машина, а не стековая машина, это немного более низкий уровень, он типизирован, и некоторые детали, которые скрыты байт-кодом Python, но важны для производительности (особенно подсчет ссылок), являются явно представлено в HIR. HIR преобразуется в форму SSA, над ним выполняются некоторые проходы оптимизации, а затем в него автоматически вставляются операции подсчета ссылок согласно метаданным о счетчике ссылок и эффектах памяти опкодов HIR.
Затем HIR понижается до промежуточного представления низкого уровня (LIR), которое представляет собой абстракцию над сборкой, реализованную в Jit/lir/
. В LIR мы выделяем регистры, проходят некоторые дополнительные оптимизации, а затем, наконец, LIR опускается до ассемблера (в Jit/codegen/
) с использованием превосходной библиотеки asmjit.
JIT находится на ранней стадии. Хотя он уже позволяет устранить накладные расходы на циклы интерпретатора и значительно повысить производительность многих функций, мы только начали прикасаться к возможной оптимизации. Многие распространенные оптимизации компилятора еще не реализованы. Наша приоритезация оптимизации во многом определяется характеристиками рабочей нагрузки Instagram.
Строгие модули — это несколько вещей в одном:
1. Статический анализатор, способный проверить, что выполнение кода верхнего уровня модуля не будет иметь побочных эффектов, видимых за пределами этого модуля.
2. Неизменяемый тип StrictModule
который можно использовать вместо типа модуля Python по умолчанию.
3. Загрузчик модулей Python, способный распознавать модули, включенные в строгий режим (с помощью import __strict__
в верхней части модуля), анализировать их на предмет отсутствия побочных эффектов импорта и заполнять их в sys.modules
как объект StrictModule
.
Статический Python — это компилятор байт-кода, который использует аннотации типов для создания специализированного по типу и проверенного типа байт-кода Python. При использовании вместе с Cinder JIT он может во многих случаях обеспечивать производительность, аналогичную MyPyC или Cython, предлагая при этом опыт разработчика на чистом Python (обычный синтаксис Python, без дополнительного этапа компиляции). Статический Python в сочетании с Cinder JIT обеспечивает 18-кратную производительность стандартного CPython в типизированной версии теста Ричардса. В Instagram мы успешно использовали Static Python в производстве для замены всех модулей Cython в кодовой базе нашего основного веб-сервера без снижения производительности.
Компилятор Static Python построен на основе модуля compiler
Python, который был удален из стандартной библиотеки в Python 3 и с тех пор поддерживается и обновляется извне; этот компилятор включен в Cinder в Lib/compiler
. Компилятор Static Python реализован в Lib/compiler/static/
, а его тесты — в Lib/test/test_compiler/test_static.py
.
Классам, определенным в модулях Static Python, автоматически предоставляются типизированные слоты (на основе проверки их атрибутов типизированного класса и аннотированных присвоений в __init__
), а при загрузке и сохранении атрибутов для экземпляров этих типов используются новые коды операций STORE_FIELD
и LOAD_FIELD
, которые в JIT становятся прямыми. загружает/сохраняет из/в фиксированное смещение памяти в объекте без каких-либо косвенных действий со стороны LOAD_ATTR
или STORE_ATTR
. Классы также получают виртуальные таблицы своих методов для использования кодами операций INVOKE_*
упомянутыми ниже. Поддержка этих функций во время выполнения находится в StaticPython/classloader.h
и StaticPython/classloader.c
.
Статическая функция Python начинается со скрытого пролога, который проверяет, соответствуют ли типы предоставленных аргументов аннотациям типов, и в противном случае выдает TypeError
. Вызовы статической функции Python к другой статической функции Python пропустят этот код операции (поскольку типы уже проверены компилятором). Статические вызовы также позволяют избежать большей части накладных расходов, связанных с типичным вызовом функции Python. Мы излучаем код операции INVOKE_FUNCTION
или INVOKE_METHOD
который содержит метаданные о вызываемой функции или методе; это плюс опционально неизменяемые модули (через StrictModule
) и типы (через cinder.freeze_type()
, которые в настоящее время мы применяем ко всем типам в строгих и статических модулях в нашем загрузчике импорта, но в будущем могут стать неотъемлемой частью Static Python) и компилируем Знание сигнатуры вызываемого объекта позволяет нам (в JIT) превращать многие вызовы функций Python в прямые вызовы по фиксированному адресу памяти с использованием соглашения о вызовах x64 с небольшими издержками, чем при вызове функции C.
Статический Python по-прежнему типизируется постепенно и поддерживает код, который аннотирован лишь частично или использует неизвестные типы, возвращаясь к нормальному динамическому поведению Python. В некоторых случаях (например, когда значение статически неизвестного типа возвращается из функции с аннотацией возврата) вставляется код CAST
времени выполнения, который вызывает TypeError
, если тип времени выполнения не соответствует ожидаемому типу.
Static Python также поддерживает новые типы машинных целых чисел, логических чисел, двойных чисел и векторов/массивов. В JIT они обрабатываются как неупакованные значения, и, например, примитивная целочисленная арифметика позволяет избежать всех накладных расходов Python. Некоторые операции со встроенными типами (например, индекс списка или словаря или len()
) также оптимизированы.
Cinder поддерживает постепенное внедрение статических модулей с помощью строгого/статического загрузчика модулей, который может автоматически обнаруживать статические модули и загружать их как статические с помощью кросс-модульной компиляции. Загрузчик будет искать аннотации import __static__
и import __strict__
в верхней части файла и соответствующим образом компилировать модули. Чтобы включить загрузчик, у вас есть один из трех вариантов:
1. Явно установите загрузчик на верхнем уровне вашего приложения с помощью from cinderx.compiler.strict.loader import install; install()
.
PYTHONINSTALLSTRICTLOADER=1
в вашей среде../python -X install-strict-loader application.py
. Альтернативно, вы можете скомпилировать весь код статически, используя ./python -m compiler --static some_module.py
, который скомпилирует модуль как статический Python и выполнит его.
См. CinderDoc/static_python.rst
для получения более подробной документации.