«Python медленный» — утверждение, с которым спорить бессмысленно и опираться на которое бесполезно. Медленный по сравнению с чем, в какой части работы и насколько — вот вопросы, на которые отвечать стоит, потому что от ответа зависит, что делать.
Тупик: переписать горячий кусок на другом языке
Первое, что пробуют, когда сервис не укладывается в отклик, — вынести тяжёлое место в расширение на компилируемом языке. Иногда это правильный ход. Как первое действие — почти всегда нет: чаще всего это не помогает, и ниже видно, почему.
Причина в том, что «горячее место» обычно определяют на глаз, и глаз ошибается. В сервисе, который ходит в базу и к соседям, время уходит на ожидание, а не на вычисление. Переписав вычислительный кусок, вы ускорите долю, которая и так занимала малую часть, а ожидание останется прежним. Работа сделана, счёт оплачен, отклик не изменился.
Второй частый ход — заменить интерпретатор на более быстрый или включить экспериментальный режим. Это может дать выигрыш, но он одинаковый по всей программе и не трогает структуру: если время уходит на лишние походы в базу, ускоренный интерпретатор будет делать те же лишние походы, просто чуть бодрее.
Общая ошибка обоих ходов одна: они начинаются с решения, а не с измерения.
Есть и третья попытка, самая безобидная на вид: раскидать нагрузку по процессам и добавить машин. Она честно работает там, где программа упирается в процессор, и не работает там, где она ждёт: восемь процессов, ожидающих ответа базы, ждут ровно столько же, сколько один, зато соединений к базе теперь в восемь раз больше. Регулярно после такого «ускорения» база становится новым узким местом, и разбираться приходится уже с двумя проблемами вместо одной.
Три разных «медленно»
Под одним словом прячутся три несвязанные ситуации, и лечатся они по-разному.
Медленно, потому что ждём. Сервис большую часть времени сидит в ожидании ответа от базы, внешнего сервиса или диска. Язык здесь почти ни при чём: заменив его, вы будете ждать столько же. Лечится сокращением числа ожиданий и их совмещением, а не скоростью вычислений.
Медленно, потому что считаем. Программа действительно занята процессором: перебирает, сравнивает, преобразует. Вот здесь язык имеет значение, и здесь же работают библиотеки, у которых вычислительное ядро написано на компилируемом языке.
Медленно, потому что делаем лишнее. Самая частая и самая недооценённая ситуация. Данные загружаются целиком, чтобы взять из них три поля. Один и тот же результат вычисляется в цикле. Список перебирается заново там, где хватило бы одного прохода. Здесь ни язык, ни библиотеки не помогут — помогает убрать лишнее.
Прежде чем что-то ускорять, надо понять, в какой из трёх ситуаций вы находитесь. Это единственное действие, которое нельзя пропустить.
Как это выяснить
Профилировщик отвечает на вопрос точно, и запускать его надо на нагрузке, похожей на боевую, а не на игрушечном примере.
Смотреть надо на две вещи. Во-первых, на распределение времени между ожиданием и работой процессора: если процессор большую часть времени простаивает, вы в первой ситуации. Во-вторых, на функции, где время накапливается, — не по одному вызову, а суммарно. Функция, отрабатывающая мгновенно, но вызванная миллион раз, стоит дороже медленной, вызванной однажды.
Отдельная полезная привычка — считать количество, а не только время. Число обращений к базе на один запрос, число проходов по коллекции, число созданных объектов. Количество объясняет причину, время только фиксирует симптом.
Мерить надо на данных того же порядка, что в бою. На тысяче записей разница между удачной и неудачной структурой данных не видна вовсе, а на миллионе она определяет всё. Оптимизация, проверенная на маленьком наборе, регулярно оказывается бесполезной, а иногда и вредной: то, что выигрывает на малых объёмах за счёт простоты, на больших проигрывает за счёт характера роста.
Что действительно ускоряет
Порядок здесь выстроен по отношению выигрыша к затраченным усилиям.
Убрать лишнюю работу. Не запрашивать данные, которые не нужны. Не вычислять дважды то, что не изменилось. Не превращать одну операцию в цикл из тысячи. Это самый дешёвый и самый недооценённый источник ускорения, потому что выглядит как уборка, а не как инженерия.
Сократить ожидания. Собрать несколько запросов в один. Совместить независимые ожидания вместо того, чтобы выстраивать их в очередь. Закешировать то, что меняется редко. В сервисах, живущих на сети, это обычно даёт больше всего.
Выбрать подходящую структуру данных. Проверка вхождения в список против проверки вхождения в множество — разница не в процентах, а в характере роста: с увеличением данных первая ухудшается, вторая нет. Такие замены стоят минуты и меняют поведение под нагрузкой качественно.
Отдать вычисления библиотеке с компилируемым ядром. Для массовой числовой и табличной работы это правильный ход: вы остаётесь на своём языке, а тяжёлая часть уходит туда, где она быстрая.
И только потом — расширение на другом языке. К этому моменту вы уже знаете, какой кусок переписывать и сколько это даст, потому что измеряли. Без этого знания переписывание — ставка вслепую.
Чем платят за ускорение
У каждого шага есть цена, и её стоит назвать заранее. Кеш добавляет вопрос о том, когда его сбрасывать, и приносит новый класс ошибок — устаревшие данные. Совмещённые ожидания усложняют обработку сбоев: раньше падало по очереди, теперь одновременно. Расширение на другом языке добавляет в сборку компилятор, усложняет выкатку и требует человека, который умеет его поддерживать.
Поэтому решение об ускорении разумно принимать не по факту «стало медленно», а по факту «медленно настолько, что это стоит дороже перечисленного».
Позиция редакции
Мы считаем, что в типичном сервисе на Python порядок работ такой: измерить, убрать лишнее, сократить ожидания, поправить структуры данных — и только после этого думать о смене языка для отдельного куска. В подавляющем большинстве случаев до последнего шага дело не доходит, потому что проблема решается раньше и дешевле.
Мы неправы, если ваша задача — сплошной счёт на процессоре без обращений наружу: обработка изображений, численное моделирование, разбор гигантских объёмов текста. Там разговор о языке уместен с самого начала, и наши советы про ожидания к вам не относятся.