Привет! Меня зовут Никита Соболев. Я занимаюсь опенсорс разработкой полный рабочий день. Тут я рассказываю про #python, #c, интересные проекты, коммиты, доклады, и тд. Поддержать: https://boosty.to/sobolevn Для связи: @sobolev_nikita
Лучший курс по Python 15: Subinterpreters
Продолжаем говорить про субинтерпретаторы.
Пригласил их автора – Eric Snow – чтобы поговорить про историю, актуальное состояние проекта и его будущее. Особо отмечу темы про будущие оптимизации. Как ускорить старт интерпретатора? Как сделать возможным шеринг большего количества данных?
Все здесь! Есть русские и английские субтитры.
А еще я добавил много примеров кода и ссылки на исходники / пепы / статьи.
Жмите на паузу и читайте :)
https://www.youtube.com/watch?v=VBiaNNpLzWA
Внутри:
00:00 Вступление
00:11 Представление гостя
01:20 Введение в subinterpreters и PEP-554
04:21 Как subinterpreters выглядят с точки зрения OS?
05:50 Зачем добавили субинтерпретаторы в Python1.5?
08:01 Сколько субинтерпретаторов можно запустить в один момент?
09:19 История subinterpreters
17:25 Изоляция модулей и PEP-687
25:22 Immortal objects и PEP-683
29:15 Static Types
32:50 Проблема с модулем SSL
34:44 Связь subinterpreters и free-threading
42:45 Erlang и Actor Model
43:50 CSP, Channels
45:23 _interpqueues
46:11 (не)Копирование данных при отправке данных в очередь
48:53 Можно ли безопасно делить все буфферы? memoryview
49:53 subinterpreters vs multiprocessing
53:09 subinterpreters and asyncio
56:07 PEP-734
56:37 Сборщик мусора, GC
58:13 Как сделать еще быстрее и лучше в будушем?
01:03:34 Какие библиотеки стоит сделать сообществу?
01:08:14 Завершение
Обсуждение: кого бы вы хотели видеть в качестве будущего гостя?
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Анонс FishITStream
(да, аллюзия на FastStream)
Мы с пацанами решили, что если и делать стримы по программированию, то сразу стримы с рыбалки. Потому что нет ничего лучше, чем говорить про программирование с рюмкой чая да на природе, глядя на озерцо.
Кто будет на связи:
– Коля Хитров — Python-блогер и серийный спикер
– Никита Пастухов — автор FastStream, галерный гребец, филантроп
– Роман Пожарнов — автор asgi-monitor, спортивный рыбак, говорит на рыбьем языке
– ну и я, конечно
О чем поговорим:
– Развитие языка Python: как развивается язык, и почему Python всё ещё лучше Go!
– Как сделать стрим из дикой природы
– Конференции и нетворкинг: зачем идти слушать и выступать, как найти тему для доклада
– Какую прикормку лучше всего брать на карася
– И, конечно же, мы не оставим без внимания OpenSource: обсудим развитие продуктов, успешные проекты, мотивацию людей и секреты правильного использования OSS
– А еще всякое про карьеру и прочее, в чем я не разбираюсь 🌚
Время: 29 июня, 12:00 МСК
Место: https://www.youtube.com/watch?v=j-XAjIlCRGg
Кстати, стрим будет на канале Коли, туда можно смело подписываться!
Надеюсь, что все получится технически.
Находки в опенсорсе: pyrefly
https://youtube.com/watch?v=7TdxFGB6LKY
Еще одно видео про еще один новый тайпчекер для питона на расте!
Много их нынче стало.
В видео:
- Обсуждаем первую версию: pyre-check
, обсудили taint analysis
- Сравниваем pyrefly
с ty
и mypy
- Смотрим на внутреннее устройство
- Применяем на реальном проекте
Ключевые ссылки из выпуска:
– Доклад о pyrefly на PyCon: https://youtu.be/ZTSZ1OCUaeQ?si=s_DPOOzsdeTk5Uqo
– pyrefly vs ty: https://blog.edward-li.com/tech/comparing-pyrefly-vs-ty (сильно советую!)
Вывод: пока очень сырой, много багов, но быстрый. Ключевой вывод: отлично, что есть конкуренция
В Python3.14 добавили новую библиотеку для сжатия: Zstandard
- PEP: https://peps.python.org/pep-0784
- Документация: https://docs.python.org/3.14/library/compression.zstd.html
- Реализация: https://github.com/python/cpython/pull/133027
Существует такой новый алгоритм для сжатия: Zstandard c хорошим процентом сжатия и быстрым алгоритмом сжатия / разжатия. Его добавили в 3.14 как нативный модуль. И как раз заодно решили прибрать все другие алгоритмы в общий модуль compression.*
Теперь:
- compression.lzma
отвечает за lzma
- compression.zstd
за Zstandard
- compression.gzip
за gzip
и так далее
Пока данные новые модули просто делают re-export всех объектов из оригинальных модулей. Однако, в какой-то момент старые имена могут быть задеприкейчены.
> Any deprecation or removal of the existing modules is left to a future decision but will occur no sooner than 5 years from the acceptance of this PEP.
Как работает?
Сам алгоритм сжатия хорошо описан в соответствующем RFC. Его лучше почитать отдельно. Тем, кому такое интересно.
А мы поговорим про питоновскую часть.
Теперь питон зависит от новой опциональной библиотеки zstd.h
, что будет, если ее нет? И тут мы должны познакомиться с системой конфигурации и сборки питона.
Мы используем AutoConf или .ac
. Данный зверь – свой большой мир, который хочется как раз показать на примере. Спорим, вы не сможете с первого раза прочитать данную конструкцию?
dnl zstd 1.4.5 stabilised ZDICT_finalizeDictionary
PKG_CHECK_MODULES([LIBZSTD], [libzstd >= 1.4.5], [have_libzstd=yes], [
WITH_SAVE_ENV([
CPPFLAGS="$CPPFLAGS $LIBZSTD_CFLAGS"
CFLAGS="$CFLAGS $LIBZSTD_CFLAGS"
LIBS="$LIBS $LIBZSTD_LIBS"
AC_SEARCH_LIBS([ZDICT_finalizeDictionary], [zstd], [
AC_MSG_CHECKING([ZSTD_VERSION_NUMBER >= 1.4.5])
AC_COMPILE_IFELSE([
AC_LANG_PROGRAM([@%:@include "zstd.h"], [
#if ZSTD_VERSION_NUMBER < 10405
# error "zstd version is too old"
#endif
])
], [
AC_MSG_RESULT([yes])
AC_CHECK_HEADERS([zstd.h zdict.h], [have_libzstd=yes], [have_libzstd=no])
], [
AC_MSG_RESULT([no])
have_libzstd=no
])
], [have_libzstd=no])
AS_VAR_IF([have_libzstd], [yes], [
LIBZSTD_CFLAGS=${LIBZSTD_CFLAGS-""}
LIBZSTD_LIBS=${LIBZSTD_LIBS-"-lzstd"}
])
])
])
AC_CHECK_HEADERS([zstd.h zdict.h], [have_libzstd=yes], [have_libzstd=no])
, она проверяет наличие нужных хедеров / зависимостей для компиляции. Данный код создает несколько проверок при генерации файла ./configure
, которые позволяют проверить, есть ли такая библиотека на машине сборки../configure
потом собирается правильный Makefile
для компиляции самого питона
@MODULE__ZSTD_TRUE@_zstd _zstd/_zstdmodule.c _zstd/zstddict.c _zstd/compressor.c _zstd/decompressor.c
Makefile
, если библиотека есть:
MODULE__ZSTD_STATE=yes
MODULE__ZSTD_CFLAGS=-I/opt/homebrew/opt/zstd/include
MODULE__ZSTD_LDFLAGS=-L/opt/homebrew/opt/zstd/lib -lzstd
Modules/_zstd/_zstdmodule.o: $(srcdir)/Modules/_zstd/_zstdmodule.c $(MODULE__ZSTD_DEPS) $(MODULE_DEPS_SHARED) $(PYTHON_HEADERS); $(CC) $(MODULE__ZSTD_CFLAGS) $(PY_STDMODULE_CFLAGS) $(CCSHARED) -c $(srcdir)/Modules/_zstd/_zstdmodule.c -o Modules/_zstd/_zstdmodule.o
# ...
Makefile
просто не будет данной цели для сборки.Находки в опенсорсе: ty (red-knot)
https://www.youtube.com/watch?v=5PCP4ICoirg
Вышло видео про новый тайпчекер и lsp: ty (старое название red-knot
)
от авторов ruff
и uv
.
Пока по первым впечатлениям – бомба! Не смотря на версию 0.0.0a8
🌚
Из плюсов:
- Быстрый
- На расте
- Куча новых фичей для типов
- Полная спецификация
- Интеграция с ruff
и IDEшками
Из минусов:
- Пока есть баги (но их поправят, конечно же)
- Нет плагинов (и скорее всего никогда не будет)
- Софт от молодой и маленькой компании
- Как сделать поддержку ty
и mypy
вместе? Если использовались ty_extensions
🤷♂️
Обсуждение: а как вам проект? Успели попробовать?
| Поддержать | sobolevn">YouTube | GitHub | Чат |
PEP 758: except и except* без ()
- PEP: https://peps.python.org/pep-0758
- PR: https://github.com/python/cpython/pull/131833
В 3.14 мы теперь можем не указывать скобки в except
и except*
, когда мы ловим несколько типов исключений и не используем as
, пример:
>>> try:
... res = int(data['value'])
... except ValueError, KeyError:
... res = 0
>>> try:
... res = int(data['value'])
... except (ValueError, KeyError):
... res = 0
as
мы все еще должны использовать скобки:
>>> try:
... res = int(data['value'])
... except ValueError, KeyError as exc:
... res = 0
...
File "<python-input-3>", line 3
except ValueError, KeyError as exc:
^^^^^^^^^^^^^^^^^^^^^^^^^^^
SyntaxError: multiple exception types must be parenthesized when using 'as'
>>> try:
... res = int(data['value'])
... except (ValueError, KeyError) as exc:
... res = 0
except_block[excepthandler_ty]:
| invalid_except_stmt_indent
| 'except' e=expression t=['as' z=NAME { z }] ':' b=block {
_PyAST_ExceptHandler(e, (t) ? ((expr_ty) t)->v.Name.id : NULL, b, EXTRA) }
| 'except' ':' b=block { _PyAST_ExceptHandler(NULL, NULL, b, EXTRA) }
except_block[excepthandler_ty]:
| invalid_except_stmt_indent
| 'except' e=expressions ':' b=block {
_PyAST_ExceptHandler(e, NULL, b, EXTRA) }
| 'except' e=expression 'as' t=NAME ':' b=block {
_PyAST_ExceptHandler(e, ((expr_ty) t)->v.Name.id, b, EXTRA) }
| 'except' ':' b=block { _PyAST_ExceptHandler(NULL, NULL, b, EXTRA) }
except_block
большое правило, которое умеет парсить except
в его разных видах. excepthandler_ty
– его Cшный тип, такое упрощение для кодогенерации конечного parser.c
|
указаны варианты, какие могут быть грамматически корректные except
в Pythoninvalid_
– работают только на второй прогон парсера, чтобы показывать более качественные ошибки пользователямexcept
, потом любые выражения, но без as
except
, потом любое одно выражение, далее as
4.
, а as
был опциональный (внутри []
скобок в описании грамматики)PEP 750: t-строки в 3.14
В питон добавили еще один способ форматировать строки. Теперь – со специальным АПИ для внешних интеграций.
- PEP: https://peps.python.org/pep-0750
- Реализация: https://github.com/python/cpython/pull/132662
Основная причина: использовать f
строки удобно, но нет никакого АПИ для перехвата момента "вставки" или интерполяции значений. Например, при форматировании html или sql – требуется специальным образом делать escape для значений. И раньше код вида f"<div>{template}</div>"
представлял собой дыру в безопасности и потенциальное место для XSS.
string.templatelib.Template
Новый префикс t
не будет создавать объект str
, он будет создавать объект класса string.templatelib.Template
:
>>> user = 'sobolevn'
>>> template = t"Hi, {user}"
>>> template
Template(strings=('Hi, ', ''), interpolations=(Interpolation('sobolevn', 'user', None, ''),))
>>> from string.templatelib import Template
>>> isinstance(template, Template)
True
template
– у нас не произошло форматирование сразу. Мы создали объект, у которого есть свойства strings
и interpolations
, из которых можно собрать финальную отформатированную строку.
>>> domain = 'example.com'
>>> query = 'python string formatting is too complex'
>>> template = t'https://{domain}?q={query}'
query
, то мы будем использовать quote_plus
для его форматирования. Остальные значения – будем вставлять как есть:
>>> from string.templatelib import Template, Interpolation
>>> from urllib.parse import quote_plus
>>> def format_url(template: Template) -> str:
... parts = []
... for part in template:
... match part:
... case str() as s: # regular string
... parts.append(s)
... case Interpolation(value, expression='query'):
... parts.append(quote_plus(value))
... case Interpolation(value):
... parts.append(value)
... return ''.join(parts)
>>> format_url(template)
'https://example.com?q=python+string+formatting+is+too+complex'
Template
был отформатирован. Нами. Ручками.=
как обычно в f
строках: t'{user=}'
!r
, !s
, .2f
, тдt
строки можно конкатенировать: t'Hello' + t' , world!'
и t'Hello, ' + 'world'
rt"Hi \n!"
_PyTemplate
Template
и Interpolation
написанные на CBUILD_INTERPOLATION
и BUILD_TEMPLATE
>>> import dis
>>> user = 'sobolevn'
>>> dis.dis('t"Hi, {user}"')
0 RESUME 0
1 LOAD_CONST 2 (('Hi, ', ''))
LOAD_NAME 0 (user)
LOAD_CONST 1 ('user')
BUILD_INTERPOLATION 2
BUILD_TUPLE 1
BUILD_TEMPLATE
RETURN_VALUE
Сложности запуска Docker в CI
Когда я писал прошлый пост про работу CI в GitVerse, я получил несколько вопросов относительно: а как работает Docker-in-Docker (DinD) в таком CI? Я спросил ребят, как они планируют реализовать данную фичу в ближайшем будущем. Ответ получился очень интересным.
Со стороны задача "запустить DinD в публичном CI" не выглядит как-то архи-сложно. Однако, на деле как всегда есть нюансы.
Какие вообще есть варианты запуска DinD?
1. Можно взять docker:dind и прокинуть ему docker.sock
, а затем получить побег из курятника, и наблюдать, как пользователи получают полный доступ к машине, где гоняются другие сборки других проектов (с секретами, конечно же). Так делать совершенно точно нельзя!
Вот пример, насколько просто сбежать из такого контейнера (в самом простом случае):
# Запускаем контейнер
» docker run --name=first -v /var/run/docker.sock:/var/run/docker.sock -it docker:dind sh
# Внутри docker:
/ # ls -alh /var/run/docker.sock
srwxr-xr-x root /var/run/docker.sock
/ # hostname
700809c044d6 # <- наш текущий хост, контейнер `first`
/ # docker container ls
CONTAINER ID NAMES
e7d7857b965a other
700809c044d6 first
/ # docker exec -it other sh
/ # hostname
e7d7857b965a # <- мы получили доступ к соседнему контейнеру на хосте :(
docker.sock
и использует root
внутри контейнера. Даже если вам нужно выставить docker.sock
, то есть варианты лучше docker:dind
и запустить его с --privileged
, прокинуть ему DOCKER_TLS_CERTDIR
, запустить второй контейнер "клиент" без --privileged
, но с нужными сертификатами, и выполнять все на нем. Такой способ уже безопаснее, но все равно есть много вариантов побега и privilege escalation
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: wemake-services/wemake-python-styleguide@1.1
wemake-services/wemake-python-styleguide
и выполняет код action внутри dockerИНН:7736632467
Erid:2W5zFJeNAVn
Сайт: https://gitverse.ru/homePEP 649 и PEP 749: `__annotate__`
- https://peps.python.org/pep-0649
- https://peps.python.org/pep-0749
История
Одна из самых больших проблем Python – непродуманность аннотаций. Особенно их работа в рантайме.
В чем основная проблема?
class A:
@staticmethod
def build() -> A: # <- тут будет NameError, потому что в теле класса `A` имя `A` еще не определено
return A()
from __future__ import annotations
, что превращает все аннотации в строки на уровне компилятора:
static int
codegen_visit_annexpr(compiler *c, expr_ty annotation)
{
location loc = LOC(annotation);
ADDOP_LOAD_CONST_NEW(c, loc, _PyAST_ExprAsUnicode(annotation));
return SUCCESS;
}
from __future__ import annotations
def some():
class A: ...
def build_a() -> A:
return A()
return build_a
import typing
typing.get_type_hints(some()) # NameError: A
__annotations__
словарь во время компиляции, как раньше:
/* Every annotated class and module should have __annotations__. */
if (find_ann(stmts)) {
ADDOP(c, loc, SETUP_ANNOTATIONS);
}
__annotate__
в теле класса или функции, или модуля:
RETURN_IF_ERROR(codegen_nameop(c, loc, &_Py_ID(__annotate__), Store));
__annotate__
будет возвращать правильные аннотации для установки в __annotations__
, можно переопределить руками и проверить:
>>> class A:
... def __annotate__(format): # TODO: support all formats
... print(f'{format=}')
... return {'a': int}
>>> A.__annotations__
format=1
{'a': <class 'int'>}
format
могут быть 3 публичных вида:VALUE
– дефолт, чтобы возвращать реальные типыFORWARDREF
– чтобы возвращать annotationlib.ForwardRef для значений, которые "еще не определены", как в примере с классом A
в самом началеSTRING
– для возвращения строк, как при __future__.annotations
, смотри _Stringifierannotationlib
, который теперь резолвит аннотации самым правильным способом. Пример:
>>> from typing import TypedDict
>>> class User(TypedDict):
... email: str
... friends: User
>>> from annotationlib import get_annotations, Format
>>> get_annotations(User) # <- VALUE is default
{'email': <class 'str'>, 'friends': <class '__main__.User'>}
>>> get_annotations(User, format=Format.FORWARDREF) # <- will be able to return VALUE in this case
{'email': <class 'str'>, 'friends': <class '__main__.User'>}
>>> get_annotations(User, format=Format.STRING)
{'email': 'str', 'friends': 'User'}
__annotate__
для TypedDict
. Очень хороший пример для вашего кода.annotationlib
, inspect.get_annotations
, проблемы и сложности данной фичи. Подписывайся!PEP765: больше никакой грязи в finally
Ссылка на PEP: https://peps.python.org/pep-0765
Одна из самых сломанных частей питона была пофикшена в 3.14
В чем проблема?
Ранее такой код вел себя крайне странно:
>>> def some():
... try:
... return 1
... finally:
... return 2
>>> some()
2
return
, а код в finally
всегда выполняется – то получаем, что получаем.
>>> def other():
... try:
... 1 / 0
... finally:
... return 2
>>> other()
2
except
🫠return
, но и с break
/ continue
в циклах:
>>> def cycle():
... for i in range(2):
... try:
... i / 0
... finally:
... print(i)
... continue
... return 2
>>> cycle()
# prints: 0
# prints: 1
# returns: 2
SyntaxWarning
в 3.14:
>>> def some():
... try:
... return 1
... finally:
... return 2
<python-input-14>:5: SyntaxWarning: 'return' in a 'finally' block
SyntaxError
, в будущих версиях._Py_c_array_t cf_finally;
в ast_opt.c
typedef struct {
bool in_finally; // мы в `finally`?
bool in_funcdef; // мы в `def` или `async def`?
bool in_loop; // мы в `for`, `async for` или `while`?
} ControlFlowInFinallyContext;
finally
, функции, циклеreturn
, break
или continue
, то выполняем проверку синтаксиса; вот код для return
:
static int
before_return(_PyASTOptimizeState *state, stmt_ty node_)
{
if (state->cf_finally_used > 0) {
ControlFlowInFinallyContext *ctx = get_cf_finally_top(state);
// если нашли `return` в `finally`, но не во вложенной функции,
// то показываем warning пользователю:
if (ctx->in_finally && ! ctx->in_funcdef) {
if (!control_flow_in_finally_warning("return", node_, state)) {
return 0;
}
}
}
return 1;
}
control_flow_in_finally_warning
используем специальное АПИ для SyntaxWarning
:
static int
control_flow_in_finally_warning(const char *kw, stmt_ty n, _PyASTOptimizeState *state)
{
PyObject *msg = PyUnicode_FromFormat("'%s' in a 'finally' block", kw);
if (msg == NULL) {
return 0;
}
int ret = _PyErr_EmitSyntaxWarning(msg, state->filename, n->lineno,
n->col_offset + 1, n->end_lineno,
n->end_col_offset + 1);
Py_DECREF(msg);
return ret < 0 ? 0 : 1;
}
Находки в опенсорсе: taskiq
https://www.youtube.com/watch?v=HcZ2FAy_srM
Сегодня в опенсорсе я нашел современную замену Celery с асинхронностью и тайпхинтами.
Поговорили с автором про то, как устроена библиотека внутри:
- Как сделать универсальные интерфейсы для всех видов очередей
- Что Redis – вообще-то не очередь, и не стоит его использовать для таких задач
- Как устроена асинхронность библиотеки внутри
- Как запускать задачи по расписанию
- Как делать сложные canvas для задач с несколькими шагами
- Обсудили модульность: каждая реализация живет в отдельном пакете
Библиотека выглядит солидно! 🌚
Видео получилось коротким, но максимально информативным.
Мне нравится такой формат, буду приглашать больше авторов разных прикольных штук и обсуждать с ними устройство их технологий.
Если ваши коллеги все еще хотят делать celery таски с asyncio.run
внутри – срочно покажите им видос и уберегите от греха!
Обсуждение: используете ли вы celery все еще в своих проектах? Если нет, то на что перешли?
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Привет! Стартуем новый проект для любителей опенсорса: помогаем меинтейнерам и контрибьюторам найти друг друга.
Как оно работает?
- В данном канале меинтейнеры разных Python проектов (от CPython, mypy, Litestar до taskiq) могут в любой момент выложить простые задачки, чтобы люди могли принять участие в разработке их проекта
- Если вы хотите поработать над задачкой – напишите в самой задаче на гитхабе: "Can I work on this?", получите подтверждение меинтейнера и приступайте
- Делитесь успехами / задавайте вопросы в нашем чате @opensource_findings_chat
Если вы меинтейнер какого-то крупного проекта (>= 100 ⭐), то пишите в чат – вас добавят как админа, чтобы вы смогли постить в канал свои задачи. Чем больше – тем лучше, не забывайте ставить тег своей технологии.
Всем хорошего опенсорса!
zen browser
После недавней оказии с FireFox, я понял, что нужно менять свой браузер.
Выбор пал на zen (почти arc, но для firefox), потому что я люблю минимализм.
Что мне нужно от браузера?
- Несколько вкладок, у меня их никогда не бывает сильно много, я все их закрываю примерно раз в день
- Панель для ввода адреса с минимумом функциональности (подсказки, история, поиск)
- Минималистичный интерфейс, без лишних кнопок
- Поддержка uBlock, нескольких других похожих плагинов
- Приватность по-умолчанию
Все. Остальные фичи мне скорее мешают. Я не пользуюсь закладками, workspacе'ами, профилями, синками и тд.
Что есть в zen?
Во-первых, браузер почти полностью позволяет убрать свой интерфейс, что приятно. Теперь по пунктам:
- Hidden Tabs: можно настроить "compact mode", чтобы вкладки исчезали, когда они не нужны, нажатие cmd+b
показывает вкладки, нажатие cmd+1
открывает первую вкладку и тд
- Floating Nav Bar: После настройки панель навигации сверху исчезает, когда ей не пользуешься (открывается на cmd+t
для открытия новой вкладки и cmd+L
фокуса в текущей)
- Busy Mode: при нажатие ctrl+b
включает интерфейс, если нужно что-то найти, если идет какой-то напряженный рабочий режим
- Tab Preview: отключаемая фича, которая позволяет сделать превью страницы и быстро ее закрыть, выглядит полезно для поиска
- Split View: отключаемая фича, которая позволяет открывать две вкладки слева и справа (у меня на `alt-v`) или сверху и снизу (`alt-h`), выглядит полезно для ревью PRов на гитхабе
Ну и конечно же работают все плагины для FireFox и даже есть свои уникальные.
Сверху я все шлифанул кастомным CSS для уничтожения некоторых объектов UI, которые меня отвлекали.
Пока пробую – и мне нравится.
Обсуждение: что сейчас еще есть интересного и удобного в мире браузеров?
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Что такое GIL в Python? Вторая часть
Я не закончил! 🌚
Мы не поговорили про очень важную часть: переключение тредов интерпретатором. Треды могут быть очень долгими и не отпускать GIL, мы должны дать поработать каждому.
Простой пример, что так оно и работает:
import threading
def first():
while True:
print('first')
def two():
while True:
print('two')
a = threading.Thread(target=first).start()
b = threading.Thread(target=two).start()
first
two
first
first
two
two
two
first
first
_gil_runtime_state
:
unsigned long _PyEval_GetSwitchInterval(void)
{
PyInterpreterState *interp = _PyInterpreterState_GET();
struct _gil_runtime_state *gil = interp->ceval.gil;
assert(gil != NULL);
return gil->interval;
}
>>> import sys
>>> sys.getswitchinterval()
0.005
gil->interval
будет использовано для вызова pthread_cond_timedwait. Мы ждем interval
для передачи события (чтобы нам дали GIL) через gil->cond
. Если уходим в таймаут, значит пришло время забирать GIL на следующем выполнении байткода силой. Смотри про сигналы и PyCOND_T
тут.
MUTEX_LOCK(gil->mutex);
unsigned long interval = (gil->interval >= 1 ? gil->interval : 1);
int timed_out = 0;
COND_TIMED_WAIT(gil->cond, gil->mutex, interval, timed_out);
/* If we timed out and no switch occurred in the meantime, it is time
to ask the GIL-holding thread to drop it. */
if (timed_out && _Py_atomic_load_int_relaxed(&gil->locked)) {
PyThreadState *holder_tstate =
(PyThreadState*)_Py_atomic_load_ptr_relaxed(&gil->last_holder);
assert(_PyThreadState_CheckConsistency(tstate));
_Py_set_eval_breaker_bit(holder_tstate, _PY_GIL_DROP_REQUEST_BIT);
}
_PY_GIL_DROP_REQUEST_BIT
будет установлен, мы сможем в _Py_HandlePending передать GIL кому-то другому:
/* GIL drop request */
if ((breaker & _PY_GIL_DROP_REQUEST_BIT) != 0) {
/* Give another thread a chance */
_PyThreadState_Detach(tstate);
/* Other threads may run now */
_PyThreadState_Attach(tstate);
}
_PyThreadState_Detach(tstate)
текущий тред потеряет GIL. И снова будет ждать его при вызове _PyThreadState_Attach(tstate)
. Пока другой работает._Py_HandlePending
и когда?_CHECK_PERIODIC
. Раньше там был макрос CHECK_EVAL_BREAKER
, и его иногда забывали добавить в нужные места. Оттого события ОС не обрабатывались, GIL не переключался, было весело.
op(_CHECK_PERIODIC, (--)) {
_Py_CHECK_EMSCRIPTEN_SIGNALS_PERIODICALLY();
QSBR_QUIESCENT_STATE(tstate);
if (_Py_atomic_load_uintptr_relaxed(&tstate->eval_breaker) & _PY_EVAL_EVENTS_MASK) {
int err = _Py_HandlePending(tstate);
ERROR_IF(err != 0, error);
}
}
// Вызываем `_CHECK_PERIODIC` в конце каждого `CALL`.
macro(CALL) =
_SPECIALIZE_CALL
+ unused/2
+ _MAYBE_EXPAND_METHOD
+ _DO_CALL
+ _CHECK_PERIODIC;
PyGILState
АПИ, потому что оно не работает нормально с субинтерпретаторами PEP-734: subinterpreters и _interpqueues
Еще одной важной часть субинтерпретаторов являются очереди. Прочитать про них можно тут:
- https://peps.python.org/pep-0734/#queue-objects
- https://github.com/python/cpython/blob/main/Lib/concurrent/interpreters/_queues.py
- https://github.com/python/cpython/blob/main/Modules/_interpqueuesmodule.c
Зачем нужны очереди? Чтобы передавать объекты между интерпретаторами:
>>> from concurrent import interpreters
>>> interp = interpreters.create()
>>> queue = interpreters.create_queue()
>>> complex_object = [1, 'a', {'key': 'val'}, (2, 3), None, object()]
>>> complex_object # notice original id
[1, 'a', {'key': 'val'}, (2, 3), None, <object object at 0x101169ff0>]
>>> queue.put_nowait(complex_object)
>>> interp.prepare_main(queue=queue) # passing `queue` object to other interpreter
>>> interp.exec('print(queue.get_nowait())') # notice id change
[1, 'a', {'key': 'val'}, (2, 3), None, <object object at 0x101b054e0>]
>>> id(12)
4308538240
>>> interp.exec('print(id(12))')
4308538240
memoryview
(как и другие PyBuffer
объекты) имеет особые гарантии. Мы передаем память прям физически. Без копирования. Потому что np.array
и подобное - тоже буферы. Если бы мы копировали память буферов, то такие вычисления не отличались бы от multiprocessing
, который часть по памяти имеет проблемы. Здесь – все по-другому:
>>> b = bytearray(b'123')
>>> m = memoryview(b)
>>> queue.put_nowait(m)
>>> interp.exec('(m := queue.get_nowait()); print(m); m[:] = b"456"') # changing memory directly
<memory at 0x103274940>
>>> b # was changed in another interpreter!
bytearray(b'456')
pickle
(пока что). В планах – ускорить и упростить данный процесс:
# ex.py
>>> class Custom:
... def __init__(self, arg: int) -> None:
... self.arg = arg
... def __getstate__(self):
... print('__getstate__')
... return {'arg': self.arg}
... def __setstate__(self, state):
... print('__setstate__', state)
... self.arg = state['arg']
# main.py
>>> from ex import Custom
>>> c = Custom(1)
>>> c
<ex.Custom object at 0x101a078c0>
>>> queue.put_nowait(c)
__getstate__
>>> interp.exec('print(queue.get_nowait())') # different object
__setstate__ {'arg': 1}
<ex.Custom object at 0x103222f20>
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()
def worker_cpu(arg: tuple[int, int]):
start, end = arg
fact = 1
for i in range(start, end + 1):
fact *= i
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
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
так параллелить, хотя я пока и не пробовал.__code__.co_exceptiontable: начало
Чтобы познакомиться с таблицами обработки исключений, нам потребуется нырнуть глубоко.
Базовая идея: таблица исключений показывает, какие строки байткода покрыты обработчиками ошибок, а какие – нет. По сути – таблица неявных переходов между логическими лейблами. За счет данной технологии реализованы питоновские "zero-cost exceptions".
Возьмем для примера вот такую простую функцию:
def other(x, y):
res = None
try:
res = x / y
except ZeroDivisionError:
res = 0
finally:
print(res)
ExceptionTable:
L1 to L2 -> L3 [0]
L3 to L4 -> L6 [1] lasti
L4 to L5 -> L7 [0]
L5 to L6 -> L6 [1] lasti
L6 to L7 -> L7 [0]
L7 to L8 -> L8 [1] lasti
try
(метки c L1
по L2
невключительно) прыгаем на L3
.finally
после успешного случая:
3 L1: LOAD_FAST_BORROW_LOAD_FAST_BORROW 1 (x, y)
BINARY_OP 11 (/)
STORE_FAST 2 (res)
7 L2: LOAD_GLOBAL 3 (print + NULL)
LOAD_FAST_BORROW 2 (res)
CALL 1
POP_TOP
LOAD_CONST 1 (None)
RETURN_VALUE
finally
разложился в набор байткода прямо после тела try
. Нам не нужно никаких дополнительных манипуляций, чтобы управлять указателем на следующую инструкцию. Так происходит благодаря псевдо-инструкции SETUP_FINALLY. Так было не всегда, раньше тут был JUMP_FORWARD
, а finally
был общий для всех.finally
после обработанного ZeroDivisionError
исключения:
-- L3: PUSH_EXC_INFO
4 LOAD_GLOBAL 0 (ZeroDivisionError)
CHECK_EXC_MATCH
POP_JUMP_IF_FALSE 6 (to L5)
NOT_TAKEN
POP_TOP
5 LOAD_SMALL_INT 0
STORE_FAST 2 (res)
L4: POP_EXCEPT
JUMP_BACKWARD_NO_INTERRUPT 28 (to L2)
ZeroDivisionError
, то обрабатываем, в конце обработки ошибки прыгаем к L2
. Если ошибка другая, то прыгаем в L5
(будет ниже). Из интересного, POP_EXCEPT убирает текущее исключение из tstate->exc_info
, так исключение считается обработанным.
4 L5: RERAISE 0
-- L6: COPY 3
POP_EXCEPT
RERAISE 1
L7: PUSH_EXC_INFO
7 LOAD_GLOBAL 3 (print + NULL)
LOAD_FAST_CHECK 2 (res)
CALL 1
POP_TOP
RERAISE 0
-- L8: COPY 3
POP_EXCEPT
RERAISE 1
except
и finally
блоках. Для такого у нас есть:L3 to L4 -> L6 [1] lasti
для обработки ошибок в except
L7 to L8 -> L8 [1] lasti
для обработки ошибок в finally
other(1, 2)
: L1
-> L2
(finally)other(1, 0)
: L1
-> L3
-> L4
-> L2
(finally)other(1, 'a')
: L1
-> L3
(TypeError) -> L5
-> L7
(finally) -> L8
finally
в обработке сложных ошибок?unraisable exceptions в питоне
Мы все с вами привыкли, что в питоне можно "зарайзить" исключение в любой момент: raise Exception
Но, что если в какой-то момент времени мы не можем вызывать исключение?
Простейший пример: что произойдет при запуске такого скрипта?
# ex.py
class BrokenDel:
def __del__(self):
raise ValueError('del is broken')
obj = BrokenDel()
del obj
print('done!') # будет ли выведено?
del
вызовет ValueError
и программа завершится
» ./python.exe ex.py
Exception ignored while calling deallocator <function BrokenDel.__del__ at 0x10303c1d0>:
Traceback (most recent call last):
File "/Users/sobolev/Desktop/cpython/ex.py", line 3, in __del__
raise ValueError('del is broken')
ValueError: del is broken
done!
dealloc
для list
?
static void
list_dealloc(PyListObject *op)
{
Py_ssize_t i;
PyObject_GC_UnTrack(op); // убираем объект из отслеживания gc
if (op->ob_item != NULL) {
i = Py_SIZE(op);
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]); // уменьшаем счетчик ссылок каждого объекта в списке
}
op->ob_item = NULL;
}
PyObject_GC_Del(op);
}
PyErr_SetString(PyExc_ValueError, "some text")
NULL
как PyObject *
из соответствующих АПИ, показывая, что у нас ошибка. Если вернуть NULL
нельзя, то мы не можем поставить ошибку. А тут у нас void
и вернуть вообще ничего нельзя. Потому приходится использовать вот такой подход с unraisable exception
import atexit
def foo():
raise Exception('foo')
atexit.register(foo)
gc
В Python3.14 добавили подсветку синтаксиса в новом PyREPL
- PR: https://github.com/python/cpython/pull/133247
Выглядит прикольно. А еще можно делать свои темы, вот пример дефолтной:
theme = {
"PROMPT": colors.BOLD_MAGENTA,
"KEYWORD": colors.BOLD_BLUE,
"BUILTIN": colors.CYAN,
"COMMENT": colors.RED,
"STRING": colors.GREEN,
"NUMBER": colors.YELLOW,
"OP": colors.RESET,
"DEFINITION": colors.BOLD,
"SOFT_KEYWORD": colors.BOLD_BLUE,
"RESET": colors.RESET,
}
your_theme
export PYTHONSTARTUP='import _colorize; from your_theme import theme; _colorize.set_theme(theme)'
PYTHONSTARTUP
: https://docs.python.org/3/using/cmdline.html#envvar-PYTHONSTARTUPПервый чемпионат по опенсорсной настолке про IT: Ship IT!
Как вы можете знать, я сделал опенсорсую настолку про IT: github.com/sobolevn/ship-it-boardgame
Её можно бесплатно распечатать и поиграть. Она так же есть бесплатно в стиме для Table Top Simulator.
Игра от 2 до 5 игроков, от 30 до 45 минут, для игроков 16+
Шуточная игра для тех, кто любит IT. Первыми задеплойте все компоненты вашей архитектуры в прод и победите конкурентов! И прямо как в настоящей жизни нужно: душить и отменять своих коллег, следить за своей инфраструктурой и не деплоить в пятницу, применять sudo
, выгорать и воровать, находить ошибки и уязвимости в чужих приложения. Игра с нешуточными последствиями!
PiterPy
Но, у меня есть новости куда круче. На ближайшем PiterPy – мы устроим первый официальный чемпионат по Ship IT!
Формат: 10 минут на разминочную игру. Далее 3 полных круга, победители проходят дальше.
Ведущий: я!
Призы: за выход в финал – настольная игра. За победу – бесплатный билет на любую конференцию jug.ru по вашему выбору. 🏆
Правила: https://github.com/sobolevn/ship-it-boardgame/blob/master/ru/rules.md
Играем в душный режим с фрилансом!
Версия игры: 0.0.23
Обязательно перед игрой прочитайте правила игры, если еще не играли :) Можно предварительно потренироваться в Steam'е с друзьями или нашей группе.
Дата и время: 16 мая, на вечеринке PiterPy + IML. Начало в 19:30.
Место: Зал 4.
✅ Регистрация на игру обязательна: https://docs.google.com/forms/d/e/1FAIpQLSctYdyrcSPKFY79o4A5-1-FZxIUnoIcjHuELFTDlkoRMA4Q_w/viewform
Жду всех настольщиков на настоящую зарубу!
Если еще не купили билет, то вот промокод на 25% скидки на персональный билет: OPENSOURCEFINDINGS
А для студентов и преподавателей ВУЗов там вообще есть отдельные билеты.
Реклама. ООО «Джуг Ру Груп». ИНН 7801341446 erid 2RanymVajKX
Perforator — система непрерывного профилирования для разных языков
https://github.com/yandex/perforator
Главные фичи:
> Efficient and high-quality collection of kernel + userspace stacks via eBPF
> Scalable storage for storing profiles and binaries
> Support of unwinding without frame pointers and debug symbols on host
> Convenient query language and UI to inspect CPU usage of applications via flamegraphs
> Support for C++, C, Go, and Rust, with experimental support for Java and Python
> Generation of sPGO profiles for building applications with Profile Guided Optimization (PGO) via AutoFDO
Но самое главное – у Perforator есть режим Continuous Profiling, где на сервак ставится агент, который передает информацию о производительности всех сервисов. На что тратит всего около 1% CPU.
Очень полезный и полный пост с анонсом на хабре.
Важное ограничение: пока работает только на x86_64 Linux, ARM поддержка планируется в ближайшем будущем.
Профилируем код на Python
Нас конечно же больше всего интересует, как данная штука умеет профилировать код на питоне.
Пока что работают только версии после 3.12, потому что нативная поддержка perf
появилась именно там: https://docs.python.org/3/howto/perf_profiling.html
Смотрим доку, как профилировать питон: https://perforator.tech/docs/en/tutorials/python-profiling
Сначала собираем при помощи docker
в пару строк: https://perforator.tech/docs/en/guides/build#container
Прямо в примере в доке есть код, который будет работать неоптимально. Запустим его:
» python server.py
My pid is 53000
Serving on port 9007
sudo perforator record --pid $YOUR_PID --duration 1m --serve ":9006"
http://localhost:9006
вас будет ждать flamegraph работы скрипта.
import requests
import random
while True:
user_id = random.randint(1, 1000000)
requests.get(f"http://localhost:9007/search_user?user_id={user_id}")
PythoNN: видео с апрельского митапа
4 апреля прошел очередной #python митап в Нижнем Новгороде.
Было очень душевно и интересно.
Случился аншлаг! Пришло много нижегородцев и приехало очень много гостей: из Москвы, Питера, Кирова и других городов. Спасибо всем!
Было 4 крутых доклада:
- "Are you NATS?" – Гурбанов Михаил https://youtube.com/watch?v=atD3JVWurno
- "Почему исправление опечаток сложнее, чем кажется, и как мы с этим српавляемся" – Дмитрий Бровкин https://youtube.com/watch?v=9HRBwwaMIfA
- "Современный web с современными темплейтами" – Алексей Гончарук https://youtube.com/watch?v=lN3Pz_hUCio
- "Демистификация PostgreSQL-индексов" – Алексей Голобурдин https://youtube.com/watch?v=6kVGSLdj28k
А потом мы сидели в баре до 5 утра.
Что улучшить?
- Первый раз записывал на StreamYard, сделал плохую композицию слайдов и видео докладчика, исправим в следующий раз. Прикрепил все слайды в описании докладов – чтобы была возможность все прочитать и скопировать код
- Поработаем над звуком, сейчас он немного прыгал
Хотите присоединиться?
- Если хотите сделать доклад, пишите мне в личку – лично учу новичков выступать и делать слайды, полная свобода в выборе темы
- Если хотите просто послушать – следите за анонсами в чате и подписывайтесь sobolevn">на мой канал с записями
У нас в Нижнем – просто офигенно, всех ждем в гости! 🌆
| Поддержать | sobolevn">YouTube | GitHub | Чат |
https://www.youtube.com/watch?v=wgxBHuUOmjA
Добавил ключевое слово maybe
в Python3.14
https://github.com/python/cpython/pull/131982 🎉
wemake-python-styleguide@1.1.0
Вышла новая версия самого строго линтера для питона. Теперь еще строже!
Главная фича релиза: wps explain
CLI, которая позволяет видеть вывод информации: почему что-то запрещено, и как такое исправить.
А так же несколько новых правил:
- WPS476
не дает использовать await
в for
(потому что вы скорее всего хотите использовать asyncio.gather
, чтобы добиться асинхронности)
- WPS477
запрещает сложную комбинацию TypeVarTuple
рядом с TypeVar
с дефолтным значением: class Class[T=int, *Ts=*tuple[int, ...]]:
Ну и много разных багов поправили, куда без них.
Полный список изменений: https://github.com/wemake-services/wemake-python-styleguide/releases/tag/1.1.0
Большое спасибо участникам нашего чата за PRы, они затащили релиз 🧡
Обсуждение: каких правил в wemake-python-styleguide вам не хватает? Какие душат вас сильнее всего? Что можно улучшить?
| Поддержать | sobolevn">YouTube | GitHub | Чат |
mlut - новое слово в подходе Atomic CSS
mlut (читается как млат) - это инструмент для верстки в подходе Atomic #css, с которым можно создавать стили любой сложности. Что-то похожее на Tailwind, но по некоторым параметрам превосходит все популярные аналоги.
Atomic CSS - это методология верстки, в которой мы используем маленькие атомарные классы, каждый из которых делает одно действие. Эти классы называют утилитами. Обычно они применяет одно CSS-свойство (например, цвет текста), но не обязательно одно. Выглядит в коде это примерно так:
<button class="D-ib P1r Bgc-blue_h">
Submit
</button>
.D-ib {
display: inline-block;
}
.P1r {
padding: 1rem;
}
.Bgc-blue_h:hover {
background-color: blue;
}
flex
=> display: flex
, но flex-auto
=> flex: 1 1 auto
tracking-wide
=> letter-spacing: 0.025em
justify-*
=> content, items, self?Js-c
=> justify-self: center
Bdr
=> border-right: 1px solid
Bdrd1
=> border-radius: 1px
Ml-1/7
=>
margin-left: -14.3%
Bgc-red200_h,f
=>
.Bgc-red200_h\,f:hover,
.Bgc-red200_h\,f:focus {
/* ... */
}
@:p-c,w>=80r_D-f
=>
@media (pointer: coarse), (min-width: 80rem) {
/* ... */
}
<!-- Tailwind -->
<div class="relative -bottom-px col-span-full col-start-1 row-start-2 h-px bg-(--cardBg)"></div>
<!-- mlut -->
<div class="Ps B-1 Gc1/-1 Gcs1 Grs2 H1 Bgc-$cardBg"></div>
[@media(any-hover:hover){&:hover}]:opacity-100
text-[length:var(--myVar,1.3rem)]
supports-[margin:1svw]:ml-[1svw]
@:ah_O1_h
=>
@media (any-hover) {
.\@\:ah_O1_h:hover {
opacity: 1
}
}
Fns-$myVar?1.3
=>
font-size: var(--ml-myVar, 1.3rem);
@s_Ml1svw
=>
@supports (margin-left: 1svw) {
.\@s_Ml1svw {
margin-left: 1svw
}
}
@use 'mlut' with (
$utils-data: (
'utils': (
'registry': (
'Mil': margin-inline,
),
),
),
);
@include mlut.apply('Mil-13');
// CSS
.Mil-13 {
margin-inline: -13px;
}
Лучший курс по Python 14: Steering Council
https://www.youtube.com/watch?v=KKgsaTtezW0
Пригласил Donghee Na, одного из 5 членов Steering Council – ключевого органа управления разработкой CPython – рассказать о своей работе.
Мне кажется, что разработчикам – важно понимать, как развиваются инструменты, которыми они пользуются. Как принимаются технические решения, как происходит обсуждение. И что в таких решения можно и нужно участвовать!
Затронули крайне важные темы:
- Free-threading
- JIT
- Tail-call dispatch и faster cpython
Donghee оставил свои контакты, если кто-то хочет серьезно начать работу над free-threading.
А я получил большое удовольствие от нашего общения. Надеюсь, что вы тоже оцените.
Советую смотреть интервью с субтитрами: есть на русском 🇷🇺 и на английском 🇺🇸.
Обсуждение: если у вас есть идеи, кого из интересных гостей пригласить – пишите в чат!
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Как работает диспатчеризация байткода внутри 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
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);
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, вроде бы, завезут поддержкуНаходки в опенсорсе: mypy@2.0
Так как чуваки на бусти собрали цель в 50 человек, я сделал видео, которое обещал.
https://www.youtube.com/watch?v=vrOwcOKIIf4
Теперь "Находки в опенсорсе" еще и в видео формате!
Рассказываю, что будет в новом релизе: что сломаем, что добавим.
Пока релиз планируется где-то на вторую половину года, а я уже про него рассказываю.
Если понравится формат – поддержи видео, покажи коллеге :)
| Поддержать | sobolevn">YouTube | GitHub | Чат |
Что такое GIL в Python?
Кажется, один из золотых вопросов для всех питонистов на собеседованиях.
Обычно, на встречный вопрос "а что конкретно в питоне является GIL?" не может ответить ни один спрашивающий.
Сегодня мы закроем данный пробел в знаниях питонистов.
Global Interpreter Lock не позволяет более чем одному треду работать с Python API за раз. Его можно отключить через --disable-gil
в 3.13+, но сегодня мы про такое не будем.
Обратите внимание на ключевую фразу "c Python API". С системными треды могут и должны работать в режиме настоящей параллельности, без GIL. Что и позволяет получить ускорение при использовании threading
, когда C код поддерживает такой способ.
Знакомьтесь – вот структура GIL _gil_runtime_state
и поведение в ceval_gil.c
.
Как можно отпустить GIL?
На уровне C есть макросы: Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS, которые отпускают GIL в нужных местах. Пример из модуля mmap:
Py_BEGIN_ALLOW_THREADS
m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset);
Py_END_ALLOW_THREADS
{
PyThreadState *_save;
_save = PyEval_SaveThread();
// your code here
PyEval_RestoreThread(_save);
}
PyThreadState
является текущим состоянием треда в CPython. Внутри хранится много контекста. Нас особо сильно интересует часть с полями про GIL:
struct PyThreadState {
struct {
unsigned int initialized:1;
/* Has been bound to an OS thread. */
unsigned int bound:1;
/* Has been unbound from its OS thread. */
unsigned int unbound:1;
/* Has been bound aa current for the GILState API. */
unsigned int bound_gilstate:1;
/* Currently in use (maybe holds the GIL). */
unsigned int active:1;
/* Currently holds the GIL. */
unsigned int holds_gil:1;
} _status;
// Thread state (_Py_THREAD_ATTACHED, _Py_THREAD_DETACHED, _Py_THREAD_SUSPENDED).
int state;
// ...
}
tstate->_status.active = 0;
tstate->_status.unbound = 1;
tstate->_status.holds_gil = 0;
tstate->state = detached_state;
_gil_runtime_state
. Py_DECREF
, и в тредах есть свой refcount, который работает локально, чтобы можно было его вызывать без GIL._threadmodule.c
.
_PyThreadState_Bind(tstate);
PyEval_AcquireThread(tstate);
_Py_atomic_add_ssize(&tstate->interp->threads.count, 1);