tgoop.com/opensource_findings/916
Last Update:
PEP-734: Subinterpreters in stdlib
- PEP: https://peps.python.org/pep-0734
- Обсуждение: https://discuss.python.org/t/pep-734-multiple-interpreters-in-the-stdlib/41147
- Документация: https://docs.python.org/3.14/library/concurrent.interpreters.html
Что оно такое?
Несколько полноценных интерпретаторов работающих рядом. Какие плюсы?
- Один процесс
- Один тред, но руками можно создавать еще
- Простые данные можно шарить без необходимости pickle, сложные нужно пиклить
- По GILу на интерпретатор, все еще можно получить плюшки настоящей многозадачности по сети
- Работает с asyncio
Минусы:
- C код нужно было значительно переработать, не все C расширения поддерживаются (пока)
Получается хорошая универсальность для разных задач.
Немного истории
Есть несколько важных нетехнических аспектов про процесс создания данной фичи:
- PEP-734 и Free-Threading делают очень похожие вещи – позволяют реализовывать настоящую многозадачность, но разными способами
- Изначально субинтерпретаторы появились в 3.10 в виде только C-шного АПИ
- Есть отдельный PyPI пакет с данным кодом
- Пайтон часть в виде PEP-734 был добавлена в 3.14 уже после feature freeze
- Изначально планировалось добавить его как модуль interpreters, однако в последний момент он стал concurrent.interpreters, вот тут доступно большое обсуждение
Как работает?
Внутри довольно много разных C-шных модулей:
- Основа: https://github.com/python/cpython/blob/main/Python/crossinterp.c
- Дефиниция модуля: https://github.com/python/cpython/blob/main/Modules/_interpretersmodule.c
- Очередь для обмена сообщениями между интерпретаторами: https://github.com/python/cpython/blob/main/Modules/_interpqueuesmodule.c
- Набор примитивов: https://github.com/python/cpython/blob/main/Modules/_interpchannelsmodule.c
Но, для пользователей - важен только питоновский АПИ, что прекрасно. Он получился простым и понятным:
interp = interpreters.create()
try:
interp.exec('print("Hello from PEP-554")')
finally:
interp.close()
Давайте посмотрим на пример и замерим!
Тестирую на M2Pro 2023. Полный код тут.
Код, CPU-bound задача, считаем факториалы от числа до числа:
def worker_cpu(arg: tuple[int, int]):
start, end = arg
fact = 1
for i in range(start, end + 1):
fact *= i
Будем разбивать работу на 4 части, считать факториал первых 10000, потом вторых и тд. Вот так запускаем сабинтерпретаторы:
from concurrent.futures import InterpreterPoolExecutor
def bench_subinterpreters():
with InterpreterPoolExecutor(CPUS) as executor:
list(executor.map(worker, WORKLOADS))
Аналогично запускаем и треды с процессами.
Результаты:
Regular: Mean +- std dev: 163 ms +- 1 ms
Threading with GIL: Mean +- std dev: 168 ms +- 2 ms
Threading NoGIL: Mean +- std dev: 48.7 ms +- 0.6 ms
Multiprocessing: Mean +- std dev: 73.4 ms +- 1.5 ms
Subinterpreters: Mean +- std dev: 44.8 ms +- 0.5 ms
Субинтерпретаторы значительно ускорили данный пример. Работает даже быстрее FT 😱
А теперь для IO-bound задачи возьмем такой пример:
def worker_io(arg: tuple[int, int]):
start, end = arg
with httpx.Client() as client:
for i in range(start, end + 1):
client.get(f'http://jsonplaceholder.typicode.com/posts/{i}')
И снова
concurrent.interpreters показывают хорошее время:
Regular: Mean +- std dev: 1.45 sec +- 0.03 sec
Threading with GIL: Mean +- std dev: 384 ms +- 17 ms (~1/4 от 1.45s)
Threading NoGIL: Mean +- std dev: 373 ms +- 20 ms
Multiprocessing: Mean +- std dev: 687 ms +- 32 ms
Subinterpreters: Mean +- std dev: 547 ms +- 13 ms
Тут может показаться, что как-то не очень много перформанса у нас получилось. Но! Вспоминаем, что внутри можно создавать дополнительные треды, чтобы еще ускорить работу. А можно даже и
asyncio так параллелить, хотя я пока и не пробовал.Обсуждение: как вам фича?
| Поддержать | YouTube | GitHub | Чат |
BY Находки в опенсорсе

Share with your friend now:
tgoop.com/opensource_findings/916
