Cinder: ориентированный на производительность форк CPython в Instagram

cinderориентированныйнапроизводительностьфоркcpythonвinstagram

Cinder – это внутренняя производственная версия CPython 3.8, ориентированная на производительность Instagram. Он содержит ряд оптимизаций производительности, в том числе встроенное кэширование байт-кода, активную оценку сопрограмм, JIT-метод по-разному и экспериментальный компилятор байт-кода, который использует аннотации типов для генерации специализированного байт-кода, который лучше работает в JIT.

Для получения дополнительной информации о CPython см. README.cpython.rst .

Поддерживается ли это?

Краткий ответ: нет.

Мы сделал Cinder общедоступным, чтобы облегчить обсуждение потенциального апстриминга некоторых из этих работ в CPython и уменьшить дублирование усилий среди людей, работающих над производительностью CPython.

Cinder не дорабатывается и не документируется для кого-либо остальное использование. У нас нет ни возможностей поддерживать Cinder как независимый проект с открытым исходным кодом, ни желания, чтобы он стал альтернативой CPython. Наша цель сделать этот код доступным – это единый более быстрый CPython. Итак, пока мы запускаем Cinder в производстве, если вы решите сделать это, вы будете сами по себе. Мы не можем делать никаких обязательств по исправлению внешних отчетов об ошибках или проверке запросов на вытягивание. Мы следим за тем, чтобы Cinder был достаточно стабильным и быстрым для нашей производственной рабочей нагрузки, но мы не даем никаких гарантий относительно его стабильности, правильности или производительности для любой другой рабочей нагрузки или использования.

Тем не менее, если у вас есть опыт работы с динамическими языковыми средами и идеи, как сделать Cinder быстрее; или если вы работаете над CPython и хотите использовать Cinder в качестве источника вдохновения для улучшения CPython (или помогать восходящим частям Cinder в CPython), пожалуйста, свяжитесь с нами; мы бы хотели поболтать!

Как его построить?

Он должен строиться так же, как CPython; configure и make -j .

Cinder – это только собран или протестирован на Linux x 69; все остальное (включая OS X), вероятно, не сработает. Мы проверяем в Github CI, что он строит и проходит тесты на Fedora 69 (см. oss-build-and-test.sh и . github / workflows / cinder -oss-build-and-test.yml ), поэтому Fedora 64, вероятно, ваш лучший выбор.

Есть несколько новых тестовых целей, которые могут быть интересны. make testcinder почти такой же, как make test , за исключением того, что он пропускает несколько проблемных тестов. в нашей среде разработки. make testcinder_jit запускает набор тестов с полностью включенной JIT, так что все функции JITted. make testruntime запускает набор модульных тестов C ++ gtest для JIT. И make test_strict_module запускает набор тестов для строгих модулей (см. Ниже).

Что здесь?

Код тени

«Теневой код» или «теневой байт-код» – это наша реализация встроенного кэширования. Он наблюдает за конкретными случаями оптимизации при выполнении общих кодов операций Python и (для горячих функций) динамически заменяет эти коды операций специализированными версиями. Ядро теневого кода находится в Python / shadowcode.c , хотя реализации специализированных байт-кодов находятся в Python / ceval.c с остальной частью цикла eval. Специфичные для Shadowcode тесты находятся в Lib / test / test_shadowcode.py .

Активная оценка сопрограммы

Если вызов асинхронной функции ожидается немедленно, мы немедленно выполняем вызываемую функцию до ее первого Ждите. Если вызываемая функция достигает return без необходимости ожидания, мы сможем вернуть это значение напрямую, даже не создавая объект сопрограммы или не переходя к циклу обработки событий. Это значительная (~ 5%) оптимизация ЦП в нашей асинхронной рабочей нагрузке.

В основном это реализовано в Python / ceval.c через новый флаг vectorcall _ Py_AWAITED_CALL_MARKER , указывающий, что вызывающий абонент немедленно ожидает этого вызова. Ищите использование макроса IS_AWAITED () и этого флага vectorcall, а также функции _ PyEval_EvalEagerCoro .

Cinder JIT

The 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: 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 и не является частью Cinder. Каталог StrictModules / в Cinder – это незавершенная перезапись на C ++ анализатора побочных эффектов импорта.

Статический Python

Статический Python – экспериментальный компилятор байт-кода, который делает использование аннотаций типов для выдачи специализированного по типу и проверенного типа байт-кода Python. При использовании вместе с Cinder JIT он может обеспечивать производительность, аналогичную или Cython во многих случаях, предлагая при этом опыт разработчика на чистом Python (обычный синтаксис Python, без дополнительных шагов компиляции). Статический Python и Cinder JIT в 7 раз превышают производительность стандартного CPython в типизированной версии теста Richards. В Instagram мы успешно использовали Static Python в производстве для замены большинства модулей Cython в нашей основной кодовой базе веб-сервера без снижения производительности.

Компилятор Static Python построен на основе Модуль компилятора Python , который был удален из стандартной библиотеки в Python 3 и с тех пор поддерживается и обновляется извне; этот компилятор включен в Cinder в Lib / compiler . Компилятор Static Python реализован в Lib / compiler / static.py , а его тесты находятся в Lib / test / test_compiler / test_static. py .

Классы, определенные в статических модулях Python, автоматически получают типизированные слоты (на основе проверки их типизированных атрибутов классов и аннотированных назначений в __ init __ ), а загрузка и сохранение атрибутов для экземпляров этих типов используют новые STORE_FIELD и LOAD_FIELD коды операций, которые в JIT становятся прямыми загрузками / сохранениями из / в фиксированное смещение памяти в объекте, без каких-либо косвенных указаний на LOAD_ATTR или STORE_ATTR . Классы также получают таблицы своих методов для использования кодами операций INVOKE _ , упомянутыми ниже. Поддержка этих функций во время выполнения находится в Include / classloader.h и Python / classloader.c .

Статическая функция Python начинается с нового кода операции CHECK_ARGS , который проверяет соответствие типов предоставленных аргументов аннотациям типов, и вызывает TypeError , если нет. При вызове статической функции Python к другой статической функции Python этот код операции пропускается (поскольку типы уже проверены компилятором). Статические и статические вызовы также могут избежать значительной части накладных расходов, связанных с типичным вызовом функции Python. Мы испускаем INVOKE_FUNCTION или INVOKE_METHOD код операции, который несет с собой метаданные о вызываемой функции или методе; это плюс необязательно неизменяемые модули (через StrictModule ) и типы (через cinder.freeze_type () , которые в настоящее время мы применяем ко всем типам в строгих и статических модулях в нашем загрузчике импорта, но в будущем может стать неотъемлемая часть Static Python) и знание сигнатуры вызываемого во время компиляции позволяет нам (в JIT) превращать многие вызовы функций Python в прямые вызовы фиксированного адреса памяти с использованием x 69 соглашение о вызове с немного большими накладными расходами, чем вызов функции C.

Статический Python по-прежнему набирается постепенно и поддерживает код, который только частично аннотирован или использует неизвестные типы, возвращаясь к нормальному динамическому поведению Python. В некоторых случаях (например, когда значение статически неизвестного типа возвращается из функции с аннотацией возврата), вставляется код операции времени выполнения CAST , который вызывает TypeError , если тип среды выполнения не соответствует ожидаемому типу.

Статический Python также поддерживает новые типы для машинных целых чисел, bools, double, и векторы / массивы. В JIT они обрабатываются как неупакованные значения, и, например, примитивная целочисленная арифметика позволяет избежать всех накладных расходов Python. Некоторые операции со встроенными типами (например, индекс списка или словаря или len () ) также оптимизированы.

Cinder не поддерживает в настоящее время поставляется в комплекте с загрузчиком модулей, который может автоматически обнаруживать статические модули и загружать их как статические при кросс-модульной компиляции; в настоящее время мы делаем это с помощью нашего строгого / статического загрузчика импорта, который не является частью Cinder. В настоящее время лучший способ поэкспериментировать со статическим питоном в Cinder – использовать ./ python -m compiler --static some_module.py , который скомпилирует модуль как статический Python. и выполнить его. (Добавьте флаг - dis , чтобы также дизассемблировать его после компиляции.) Поскольку здесь не используется StrictModule или замораживания типов по умолчанию, результирующий код не будет таким оптимальным, как тот, который мы получаем в prod, особенно для вызовов функций и методов.