Донхи На и Никита Соболев предложили PEP 841: запись f{1, 2, 3} создаёт frozenset, f{'a': 1} создаёт frozendict, f{} даёт пустой frozendict. Черновик написан 16 июля, обсуждение открыли 20 июля, целятся в Python 3.16.
Сейчас frozenset({1, 2, 3}) сначала собирает обычное множество, потом копирует его в неизменяемое, и на каждом выполнении ищет имя frozenset в области видимости. Соптимизировать это компилятор не может: имя могут переопределить, а у вызова могут быть побочные эффекты. Один частный случай CPython уже спрямляет, но только справа от in. Присвойте тот же литерал переменной, и оптимизация исчезает.
Что предлагает PEP:
🔘 неизменяемость гарантирует сама грамматика, поэтому её не приходится выводить из того, как значение используется дальше;
🔘 константный f{...} сворачивается в один LOAD_CONST и попадает в .pyc, так что на каждом следующем выполнении конструирование бесплатно;
🔘 добавляются токен FLBRACE, четыре узла AST и две инструкции байткода: BUILD_FROZENSET и BUILD_FROZENMAP;
🔘 работают и генераторные выражения: f{x for x in xs}, f{k: v for k, v in items}, а также распаковка f{*xs} и f{**d};
🔘 f{ сегодня синтаксическая ошибка, поэтому старый код ничего не теряет. Запись f {1} с пробелом ошибкой и останется, путаницы с f-строками нет;
🔘 в стандартной библиотеке авторы насчитали около 105 вызовов frozenset и 65 вызовов frozendict, из них 46 и 22 можно переписать новой записью.
Большая цель за синтаксисом — подтолкнуть людей к неизменяемым контейнерам перед эпохой сборок без GIL и субинтерпретаторов: субинтерпретаторы уже делят между собой frozenset, на очереди frozendict. Про JIT авторы намеренно ничего не обещают, потому что руководящий совет в июне запретил новую разработку JIT в main до принятия отдельного PEP.
Читается ли f{1, 2, 3} лучше, чем frozenset({1, 2, 3}), или лишний префикс только запутает?
@zen_of_python
Сейчас frozenset({1, 2, 3}) сначала собирает обычное множество, потом копирует его в неизменяемое, и на каждом выполнении ищет имя frozenset в области видимости. Соптимизировать это компилятор не может: имя могут переопределить, а у вызова могут быть побочные эффекты. Один частный случай CPython уже спрямляет, но только справа от in. Присвойте тот же литерал переменной, и оптимизация исчезает.
Что предлагает PEP:
Большая цель за синтаксисом — подтолкнуть людей к неизменяемым контейнерам перед эпохой сборок без GIL и субинтерпретаторов: субинтерпретаторы уже делят между собой frozenset, на очереди frozendict. Про JIT авторы намеренно ничего не обещают, потому что руководящий совет в июне запретил новую разработку JIT в main до принятия отдельного PEP.
Читается ли f{1, 2, 3} лучше, чем frozenset({1, 2, 3}), или лишний префикс только запутает?
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
❤12🔥2😍2
Сервис убивало по нехватке памяти каждые несколько часов, но утечки в нём не было
RSS рос ступенями: 620 МБ, 890 МБ, 1,4 ГБ, 2,3 ГБ, 3,1 ГБ. Трафик, загрузка процессора и p99 при этом стояли ровно. Число объектов Python не увеличивалось, ни разбухшего словаря, ни списка, ни кэша найти не удалось.
Дело в разделении обязанностей. Сборщик мусора решает, какие объекты ещё достижимы, а аллокатор решает, вернутся ли освободившиеся страницы операционной системе. Освобождённый объект просто уходит обратно в кучу процесса, и пока в той же странице памяти лежит хоть один живой объект, отдать её ядру нельзя. У glibc
🔘 признак, отличающий это от настоящей утечки:
🔘 лечение обошлось без единой правки в коде Python, jemalloc подключается через
🔘 в продакшене к нему добавили
🔘 автор сообщает примерно о вдвое меньшем потреблении оперативной памяти после перехода;
🔘 проверить гипотезу можно и без замены аллокатора, через
Оговорка автора:
Приходилось ловить рост RSS при стабильном числе объектов, и чем в итоге объяснялось?
@zen_of_python
RSS рос ступенями: 620 МБ, 890 МБ, 1,4 ГБ, 2,3 ГБ, 3,1 ГБ. Трафик, загрузка процессора и p99 при этом стояли ровно. Число объектов Python не увеличивалось, ни разбухшего словаря, ни списка, ни кэша найти не удалось.
gc.collect() честно собирал циклический мусор, а RSS после него почти не опускался. Сакшам Шарма разобрал этот случай 8 июля.Дело в разделении обязанностей. Сборщик мусора решает, какие объекты ещё достижимы, а аллокатор решает, вернутся ли освободившиеся страницы операционной системе. Освобождённый объект просто уходит обратно в кучу процесса, и пока в той же странице памяти лежит хоть один живой объект, отдать её ядру нельзя. У glibc
malloc к этому добавляются кэш освобождённых блоков tcache и отдельные арены на каждый поток, так что многопоточный сервис держит несколько независимых запасов. В одном из примеров автора живые объекты занимали около 700 МБ при RSS около 2,8 ГБ.tracemalloc показывает стабильный объём, а RSS продолжает расти;LD_PRELOAD;PYTHONMALLOC=malloc и MALLOC_CONF=narenas:2,background_thread:true;MALLOC_ARENA_MAX=2 и вызов malloc_trim(0).Оговорка автора:
malloc не универсально лучше встроенного pymalloc, на миллионах мелких объектов выигрыш может оказаться обратным, поэтому замену нужно мерить на своей нагрузке.Приходилось ловить рост RSS при стабильном числе объектов, и чем в итоге объяснялось?
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
uv 0.12.0:
Astral выпустила uv 0.12.0 28 июля, первый крупный релиз после 0.11.0 в марте. Изменения касаются дефолтов инициализации, проверки хэшей и разбора архивов пакетов.
Раньше
🔘
🔘 отклоняются исходные дистрибутивы в форматах
🔘 wheel, способный перезаписать сам интерпретатор (файлы вида
🔘 режим предрелизов по умолчанию сменился с
🔘 директива
🔘 строже валидируется
Авторы пишут, что большинству обновление не потребует правок. Исключение составляют проекты, где зафиксирована верхняя граница сборочного бэкенда: там нужно поднять до
У вас в CI uv закреплён конкретной версией или подтягивается последний?
@zen_of_python
uv init теперь создаёт проект со сборочной конфигурациейAstral выпустила uv 0.12.0 28 июля, первый крупный релиз после 0.11.0 в марте. Изменения касаются дефолтов инициализации, проверки хэшей и разбора архивов пакетов.
Раньше
uv init создавал плоскую структуру без сборочной системы, и сборочную конфигурацию со структурой src приходилось добавлять отдельно. Теперь проект сразу пригоден для сборки.uv init прописывает [build-system] на uv_build, кладёт код в src/<name> и добавляет точку входа в [project.scripts], прежнее поведение возвращает флаг --no-package;.tar.bz2 и .tar.xz, по PEP 625 допустим .tar.gz, старые .zip пока принимаются ради совместимости; заодно запрещены записи со сжатием bzip2 и LZMA внутри wheel;Python, python.py, Python.exe, записи в .data/scripts), больше не устанавливается;if-necessary-or-explicit на if-necessary, то есть предрелизная версия ставится только когда без неё зависимости не разрешаются;--require-hashes внутри requirements.txt действительно включает проверку хэшей, а хэши только по MD5 отвергаются, нужен минимум SHA-256;pylock.toml: обязателен массив packages, имя файла допустимо только вида pylock.toml или pylock.dev.toml, проверяется размер артефактов.Авторы пишут, что большинству обновление не потребует правок. Исключение составляют проекты, где зафиксирована верхняя граница сборочного бэкенда: там нужно поднять до
uv_build>=0.11.32,<0.13.У вас в CI uv закреплён конкретной версией или подтягивается последний?
@zen_of_python
Please open Telegram to view this post
VIEW IN TELEGRAM
Rust научился ускорять операции с плавающей точкой, сохраняя контроль за точностью
Если вы держите Rust под рукой для кусков, где Python не хватает скорости, в Rust 1.98 появился повод присмотреться. Новый API разрешает компилятору агрессивнее оптимизировать операции с плавающей точкой, но сохраняет контроль за точностью округления. Это значит, что Rust-код, который вызывается из Python, может считать быстрее без привычного компромисса «скорость против корректности».
Раньше в Rust не было стабильного способа управлять этим компромиссом. Теперь можно явно указать компилятору, где безопасно ускорять вычисления, а где важна минимальная погрешность.
Разобрался Itamar Turner-Trauring: если пишете Rust-расширения для Python или интересуетесь, откуда берётся скорость в числовых библиотеках, загляните.
Если вы держите Rust под рукой для кусков, где Python не хватает скорости, в Rust 1.98 появился повод присмотреться. Новый API разрешает компилятору агрессивнее оптимизировать операции с плавающей точкой, но сохраняет контроль за точностью округления. Это значит, что Rust-код, который вызывается из Python, может считать быстрее без привычного компромисса «скорость против корректности».
Раньше в Rust не было стабильного способа управлять этим компромиссом. Теперь можно явно указать компилятору, где безопасно ускорять вычисления, а где важна минимальная погрешность.
Разобрался Itamar Turner-Trauring: если пишете Rust-расширения для Python или интересуетесь, откуда берётся скорость в числовых библиотеках, загляните.
❤1👍1