
Python 3.15 отримав вбудований профайлер Tachyon: він підключається до працюючого процесу за ідентифікатором (PID) і показує, де програма витрачає час, без змін у коді й перезапуску. Станом на 4 жовтня 2026 року вийшов третій реліз-кандидат 3.15.0rc3, а фінальний випуск розробники перенесли на 9 жовтня через блокувальні помилки у відкладених імпортах.
Для програміста, який пише на Python щодня, це закриває давню прогалину: стандартна бібліотека вперше має інструмент, придатний для діагностики живого сервісу. Він живе в модулі profiling.sampling, що з'явився разом із новим пакетом profiling за PEP 799 (Python Enhancement Proposal, пропозиція щодо вдосконалення Python).
Вибіркове і трасувальне профілювання
Класичні cProfile і profile належать до трасувальних профайлерів: вони реєструють кожен виклик функції й повернення з неї. Кількість викликів виходить точною, але інтерпретатор виконує додаткову роботу на кожному кроці. Вибірковий профайлер діє інакше: з фіксованою частотою зчитує стек викликів процесу ззовні й рахує, які функції трапляються найчастіше.
Частота за замовчуванням становить 1 кГц, тобто тисяча знімків на секунду. Час у звіті є оцінкою: кількість семплів, помножена на інтервал. У прикладі з документації за 10 секунд на частоті 10 кГц набирається близько 100 000 семплів, і якщо функція трапилась у 5 000 із них, це приблизно 5% або 500 мілісекунд. Похибка при такій вибірці становить близько ±0,5%, а між запусками цифри трохи різнитимуться, тож дивіться на загальну картину.
Профілювання відповідає на запитання, де саме губиться час, і тільки після цього оптимізація має сенс.
Різницю між двома підходами зручно побачити в таблиці.
| Критерій. | profiling.sampling (Tachyon). | profiling.tracing (cProfile). |
|---|---|---|
| Принцип. | Періодичні знімки стека ззовні. | Запис кожного виклику й повернення. |
| Накладні витрати. | Майже нульові для цільової програми. | Додаткові витрати на кожен виклик. |
| Кількість викликів. | Оцінка за семплами. | Точні значення. |
| Підключення до запущеного процесу. | Так, за PID. | Ні, профайлер вмикають під час старту або в коді. |
| Типове завдання. | Пошук вузьких місць, діагностика живого сервісу. | Точні лічильники й детальна статистика. |
Модуль profile тепер застарілий і буде видалений у Python 3.17, а cProfile лишається псевдонімом нового profiling.tracing, тож старий код не зламається.
Що таке семпл і flame graph
Семпл — один знімок стека викликів у момент опитування процесу. Flame graph — діаграма, де кожна функція зображена прямокутником, а його ширина пропорційна кількості семплів, у яких функція траплялася.
Перший запуск у терміналі
Для експерименту візьмемо скрипт зі свідомо повільним місцем: видалення дублікатів через пошук у списку. Кожна перевірка r not in result проходить список повністю, тому витрати ростуть квадратично.
import random def load_rows(n): return [random.randint(0, n // 2) for _ in range(n)] def unique_slow(rows): result = [] for r in rows: if r not in result: # лінійний пошук у списку result.append(r) return result def summarize(rows): return sum(rows) / len(rows) if __name__ == "__main__": rows = load_rows(40000) print(len(unique_slow(rows)), summarize(rows))
Запускаємо профайлер під інтерпретатором 3.15 (для перевірки підійде й 3.15.0rc3):
python -m profiling.sampling run report.py
За замовчуванням звіт виводиться таблицею з колонками nsamples, sample%, tottime, cumul% і cumtime. У nsamples числа записані як «прямі/сукупні»: прямі семпли набираються, коли функція була нагорі стека й виконувалась сама, сукупні — коли вона просто була в ланцюжку викликів.
У нашому скрипті очікувано, що unique_slow опиниться вгорі таблиці, а її прямі й сукупні семпли майже збігатимуться. Документація описує таку картину як гарячу точку, де процесор працює безпосередньо в тілі функції.
Для наочності додайте --flamegraph -o profile.html: сторінка відкривається в браузері без зовнішніх залежностей, ширина прямокутника пропорційна часу, а клік наближає потрібну гілку. Режим --heatmap накладає кількість семплів прямо на рядки вашого коду.
Робочий цикл оптимізації складається з чотирьох кроків:
- Запустіть скрипт під профайлером і знайдіть функцію з найбільшою кількістю прямих семплів.
- Збережіть базовий профіль у бінарному файлі, щоб було з чим порівнювати.
- Виправте код, наприклад замініть список на множину для перевірки наявності елемента.
- Запустіть скрипт ще раз і порівняйте результат із базовим профілем.
Останні два кроки зручно виконувати не на око, а через диференціальний flame graph.
Порівняння до і після виправлення
Базовий профіль записуємо у бінарний формат, а після зміни коду запускаємо профайлер із прапорцем порівняння.
python -m profiling.sampling run --binary -o baseline.bin report.py # виправляємо unique_slow і запускаємо ще раз python -m profiling.sampling run --diff-flamegraph baseline.bin -o diff.html report.py
На діаграмі червоним позначено функції, що почали споживати більше часу, синім — ті, що стали швидшими, сірим — без змін, фіолетовим — нові. Колір показує зміну прямого часу, тобто часу, коли функція була вгорі стека. Гілки, які зникли після оптимізації, можна переглянути окремо за перемикачем «elided».
Режими wall, cpu і gil
Один і той самий скрипт можна виміряти в різних режимах, і різниця між ними сама підказує причину повільності. Параметр --mode має чотири значення:
- wall — усі семпли, зокрема очікування й сон;
- cpu — лише виконання на ядрі процесора;
- gil — лише моменти, коли потік тримає глобальне блокування інтерпретатора (GIL);
- exception — лише потоки з активним винятком.
GIL дозволяє виконувати байткод лише одному потоку за раз. Wall вмикається за замовчуванням, і саме порівняння його з cpu найкраще відокремлює обчислення від очікування. Якщо функція високо і в одному профілі, і в другому, вона справді навантажує процесор, тож варто шукати кращий алгоритм.
Якщо в wall вона на вершині, а в cpu майже зникає, програма чекає на зовнішній ресурс. Тут допоможуть асинхронний ввід-вивід, пул з'єднань або скорочення очікувань. У прикладі з документації скрипт спить дві секунди й рахує суму квадратів: у wall переважає сон (близько 98% семплів), а в cpu сон відсутній і вгорі опиняється обчислення.
Режим gil потрібен для багатопотокових програм: він показує, які функції монополізують інтерпретатор і через що інші потоки не отримують часу. Нативний код розширень, як-от обчислення хешів, зазвичай звільняє GIL, тому в тому ж прикладі хешування займає близько 42% у режимі cpu і лише близько 5% у gil.
Підключення до живого процесу
Головна перевага Tachyon — команда attach: вона підключається до вже запущеного процесу за PID, збирає семпли заданий час і відключається. Застосунок не зупиняється й не відчуває спостереження. Розробники радять починати з короткого вікна в 10–30 секунд і знімати профіль під типовим навантаженням, а не під піком трафіку.
python -m profiling.sampling attach -d 30 --flamegraph -o profile.html 12345 python -m profiling.sampling dump 12345
Команда dump робить один знімок і виводить стек у вигляді трейсбеку з позначками стану кожного потоку, зокрема тримає він GIL чи працює на процесорі. Це зручно, коли сервіс завис і потрібно швидко побачити, на чому саме.
Операційна система не дозволяє читати пам'ять чужого процесу без дозволу, тому для attach потрібні права. У Linux це запуск від root, можливість CAP_SYS_PTRACE або зміна параметра ядра ptrace_scope, значення 1 за замовчуванням обмежує доступ батьківськими процесами. У macOS потрібні root або спеціальний дозвіл для налагоджувачів, у Windows — права адміністратора.
Є й обмеження за версіями: профайлер і цільовий процес мають працювати на одній мінорній версії Python. Підключитися з 3.14 до 3.15 неможливо, а для передрелізних збірок потрібен однаковий точний випуск. Для процесів на 3.12–3.14 лишається сторонній py-spy, який, за матеріалами подкасту Talk Python, вміє підключатися й до цих версій, хоч і з різною продуктивністю.
Для продакшну варто дочекатися фінального 3.15.0: сам реліз-кандидат розробники не рекомендують для промислових середовищ.
Що Tachyon не показує
Tachyon бачить лише кадри Python. Якщо більшу частину часу займає NumPy, pandas чи інше розширення на C, у звіті з'явиться функція, що його викликала, але не те, що відбувається всередині. Параметр --native додає позначки переходів у нативний код, проте детальний аналіз нативного коду потребує окремого інструмента.
Є й інші межі. Для скриптів, що виконуються менше секунди, семплів може не вистачити: використайте profiling.tracing або запустіть код у циклі. Точні лічильники викликів дає лише трасувальний профайлер, а різницю в 1–2% між двома варіантами краще міряти через timeit, бо шум вибірки її заховає.
Для асинхронного коду є режим --async-aware: він відновлює стеки за ланцюжками await, а не за внутрішніми кадрами циклу подій. Програми з multiprocessing підтримує параметр --subprocesses, який запускає окремий профайлер для кожного дочірнього процесу Python і створює окремий файл результатів.
Tachyon добре доповнює cProfile: вибірковий профайлер знаходить вузьке місце, трасувальний уточнює деталі. Разом із ним у 3.15 з'являться відкладені імпорти, вбудований frozendict і UTF-8 як кодування за замовчуванням. Напишіть у коментарях, на якому скрипті чи сервісі ви першим запустите Tachyon і що він покаже.
Схожі публікації