Pull to refresh

Ускорение Python-скриптов без приложения умственных усилий

Reading time 3 min
Views 26K
Одно из распространенных применений Python — небольшие скрипты для обработки данных (например, каких-нибудь логов). Мне часто приходилось заниматься такими задачами, скрипты обычно были написаны наспех. Вкупе с моим слабым знанием алгоритмов это приводило к тому, что код получался далеко не оптимальным. Это меня ничуть ни расстраивало: лишняя минута выполнения не сделает погоды.

Ситуация немного изменилась, когда объем данных для обработки вырос. И после того, как время выполнения очередного скрипта перевалило за сутки, я решил уделить немного времени оптимизации — все-таки хотелось бы получить результат до того, как он потеряет актуальность. В рамках этой статьи я не планирую говорить о профилировании, а затрону тему компиляции Python-кода. При этом обозначу условие: варианты оптимизации не должны быть требовательными к времени разработчика, а, напротив, быть дружественными к «пыщ-пыщ и в продакшен».

Для бенчмарка я сделал два скрипта, решающих абсолютно синтетические задачи:
  • generate.py — скрипт генерирует 500 тыс. словарей с одинаковыми ключами и разными значениями, сериализует их в json и записывает в файл. Получается что-то вроде:


    {"str_youCPQmO": "TMrjhoKpFuyZ", "int_VAuUXXmC": 5, "int_ScRduCVX": 73, "str_YfsEUEve": "IOAYDoAuZBzQ", "int_dlBzZYlO": 77, "int_lJaDHVSH": 45, "str_qzDCDxbm": "rfERFTVFZiku", "int_gblmMsBX": 57, "str_MZNfINjj": "DeDaDMjKQyzo", "str_sUQVbIyn": "tenhduEcWkof"}
    
{"str_youCPQmO": "OJRZDmiQxflr", "int_VAuUXXmC": 9, "int_ScRduCVX": 32, "str_YfsEUEve": "CYxuIUTWAVTH", "int_dlBzZYlO": 37, "int_lJaDHVSH": 22, "str_qzDCDxbm": "aZTizzobHBbh", "int_gblmMsBX": 63, "str_MZNfINjj": "sJElOjzNlpJZ", "str_sUQVbIyn": "WDUdOMwERjxm"}
 
    
  • analyze.py — скрипт считывает сгенерированный выше файл и агрегирует их двумя способами:
    1. если значения являются строками, то нужно найти наиболее используемый символ по этому ключу;
    2. если значения являются числами, то нужно посчитать среднее значение сигмоидной функции от каждого из них (довольно странно, но почему бы и нет?).

Задачи, будучи выдуманными, тем не менее похожи на то, с чем я сталкивался в реальной жизни, так что для бенчмарка вполне подойдут. Хотя очевидно, что в обычной работе вмешиваются дополнительные факторы: скрипт нужно распараллелить, можно использовать специализированные библиотеки (гораздо проще и быстрее агрегировать числа при помощи numpy / pandas) и т.д.

Поскольку я с самого начала выставил себе требование, что способ ускорения должен быть простым, остаются только варианты, для реализации которых достаточно по диагонали пролистать мануалы. Поэтому я бегло погуглил что-то вроде ‘jit python’, ‘compiler python’ и выбрал для бенчмарка:

  • python 2.7 без сторонних библиотек (эталон);
  • python 3 без сторонних библиотек;
  • pypy;
  • nuitka —recurse-none (компилируются только основные файлы);
  • nuitka —recurse-all (компилируются все зависимости);
  • numba;
  • Cython без модификации кода под статическую типизацию;

Очевидно, что сравнение не очень корректное — в списке и разные интерпретаторы, и компиляторы, как jit, так и обычные. Тем не менее, все они могут рассматриваться для решения моей прикладной задачи — малой кровью добиться того, чтобы скрипты отрабатывали быстрее, чем их результат потеряет актуальность.

К сожалению, запустить numba мне не удалось — скрипты падали с исключениями типа NotImplementedError: cell vars are not supported. Кажется, как универсальное средство ускорения всего подряд numba пока не подойдет.

Бенчмарк проводился на Macbook Pro Late 2013 (2.4 GHz Intel Core i5). Для запуска был написан небольшой fabric-скрипт, так что желающие могут легко повторить в интересующих их условиях. Итак, результат:


Как видно, прирост производительности меняется от скрипта к скрипту (что неудивительно). Тем не менее можно отметить безоговорочную победу pypy в обеих номинациях, некоторое ускорение от Cython и бессмысленности использование nuitka для этих целей (что не отменяет возможности использования, если, например, просто нужно собрать воедино все зависимости). Также интересно, что для агрегации python 3 оказался быстрее cythonized-версии такого же скрипта. Я для себя решил, что в разных случаях резонно использовать и pypy, и Cython: например, если в скрипте вовсю используются numpy/scipy/pandas etc., то с pypy могут возникнуть сложности (не весь этот стек работает из коробки с pypy), а вот транслировать одну тяжелую функцию в Cython будет довольно легко.

Исходники теста лежат на Github, улучшения и дополнения приветствуются.
Tags:
Hubs:
+9
Comments 2
Comments Comments 2

Articles