Используйте Psyco, и Python будет работать так же быстро, как и С
🕛 17.09.2009, 13:46
Используя Psyco, компилятор обработки PythonВ некоторых отношениях дизайн Python напоминает дизайн Java. Оба используют виртуальную машину, которая интерпретирует псевдотранслируемый байткод. Область, в которой виртуальная машина Java обошла Python - это оптимизация выполнения байткода. Psyco, компилятор обработки Python, помогает "сравнять счет". Сейчас Psyco - это внешний модуль, но когда-нибудь он может быть включен в сам Python. Совсем чуть-чуть дополнительного программирования, и Psyco можно использовать, чтобы на порядок ускорить код Python. В этой статье Дэвид рассматривает, что есть Psyco, а также тестирует его в некоторых приложениях.
Обычно Python достаточно быстро делает то, что вы хотите. Девяносто процентов вопросов, которые возникают у начинающих программистов касательно скорости исполнения интерпретируемых/ байткомпилируемых языков, подобных Python, просто наивны. На современных аппаратных средствах большинство неоптимизированных программ Python исполняются так быстро, как от них требуется, и в действительности нет никакого смысла тратить дополнительные усилия на программирование, чтобы приложение работало быстрее.
В этой статье, следовательно, я коснусь только оставшихся десяти процентов. Время от времени программы на Python (или программы на других языках программирования) выполняются недопустимо медленно. Решаемые вопросы могут быть самыми разными; редко требуется выигрыш в миллисекунды, но ускорение задач, которые выполняются в течение минут, часов, дней или недель часто стоит усилий. Более того, необходимо заметить, что не все, что работает медленно, вызвано центральным процессором. Если, например, на выполнение запроса к базе данных требуются часы, не имеет особого значения, обрабатывается ли результирующий набор данных в течение минуты или двух. Эта статья также и не о проблемах ввода/вывода.
Существует несколько способов ускорить программы на Python. Первое, что должно прийти в голову любому программисту, - это улучшить алгоритмы или используемые структуры данных. Микрооптимизация шагов неумелого алгоритма -удел глупцов. Например, если порядок сложности текущей технологии O(n**2), десятикратное ускорение шагов гораздо менее полезно, чем нахождение замены O(n). Это правило применимо даже при рассмотрении такого исключительного подхода, как перепрограммирование на ассемблере: правильный алгоритм на Python часто будет выполняться быстрее, чем неверный алгоритм, переложенный вручную на ассемблер.
Вторая технология, которую вы должны рассмотреть в первую очередь - это профилирование своего приложения Python с позиции перепрограммирования ключевых частей в виде модулей расширения на С. Используя обертку расширений, как, например, SWIG (см. Ресурсы), вы можете создать расширение С, которое выполняет в виде кода на С элементы вашей программы, которые поглощают больше всего времени. Такое расширение Python относительно просто, но требует некоторого времени на освоение (и знания С). Вы увидите, что очень часто львиная доля времени, отводимого на выполнение вашего приложения на Python, затрачивается на горстку функций, и поэтому возможен значительный выигрыш.
Третья технология основывается на второй. Грэг Эуинг (Greg Ewing) создал язык Pyrex, который соединяет Python и С. В частности, чтобы воспользоваться Pyrex, вы пишете функции на схожем с Python языке, который добавляет к выбранным переменным объявления типов. Pyrex (средство) преобразует pyx-файлы в расширения ".с". После трансляции компилятором С, эти модули Pyrex (язык) могут быть импортированы и использованы в ваших обычных приложениях Python. Поскольку Pyrex использует практически такой же синтаксис, как и сам Python (включая директивы цикла, ветви и исключения, формы присвоения [assignment forms], структурированное расположение и так далее), программисту на Pyrex не нужно изучать С, чтобы писать расширения. Более того, Pyrex допускает более цельное - по сравнению с расширением, написанным непосредственно на С - смешение переменных уровня С с переменными уровня Python в пределах одного и того же кода.
Тема данной статьи - еще одна технология. Модуль расширения Psyco может встраиваться в самые недра интерпретатора Python и выборочно заменять части интерпретируемого байткода Python оптимизированным машинным кодом. В отличие от описанных выше методик, Psyco работает исключительно во время исполнения Python. Другими словами, исходный код Python транслируется командой python в байткод точно так же, как и раньше (за исключением пары директив import и вызовов функций, добавленных для запуска Psyco). Однако, пока интерпретатор Python выполняет приложение, Psyco иногда делает проверки, чтобы выяснить, может ли он заменить обычные операции байткода Python на некоторый обработанный машинный код. Эта обрабатываемая трансляция не только очень похожа на то, что делает компиляция по месту (just-in-time compilers) Java (по крайней мере, в широком смысле), но и зависит от архитектуры. В настоящее время, Psyco доступен только для архитектур с процессором i386. Прелесть Psyco заключается в том, что вы можете использовать тот же самый код Python, который вы писали всё это время (буквально!), но исполнять его быстрее.
Как Psyco работает
Чтобы полностью понять Psyco, вам, вероятно, потребуется хорошо разбираться и в функции eval_frame() интерпретатора Python, и в ассемблере i386. К сожалению, сам я не могу претендовать на роль эксперта, но думаю, что смогу объяснить Psyco в общих чертах, не допуская слишком серьезных ошибок.
В обычном Python функция eval_frame() - это внутренний цикл интерпретатора Python. В основном, функция eval_frame() смотрит на текущий байткод в контексте выполнения и переключает управление в функцию, подходящую для реализации этого байткода. Специфика того, что эта функция поддержки будет делать, зависит, в общем, от состояния различных объектов Python, находящихся в памяти. Поясним - суммирование объектов Python "2" и "3" приводит к результату, отличному от суммирования объектов "5" и "6", хотя обе операции обрабатываются одинаково.
Psyco заменяет функцию eval_frame() составным оценочным модулем. Существует несколько способов, позволяющих Psyco улучшить то, что делает Python. Во-первых, Psyco транслирует операции в до некоторой степени оптимизированной машинный код; само по себе это приводит только к незначительным улучшениям, поскольку то, что машинный код должен выполнить, - это то же самое, что делают обработанные функции Python. Более того, то, что является "обработанным" в трансляции Psyco, это больше, чем выбор байткода Python, Psyco также уточняет значения переменных, которые известны в контексте выполнения. Например, в коде, аналогичном приведенному ниже, переменная x распознаваема на протяжении цикла:
x = 5
l = []
for i in range(1000):
l.append(x*i)
Оптимизированной версии этого кода не нужно умножать каждый i на "содержимое переменной/объекта x" - менее расточительно просто умножать каждый i на 5, исключая тем самым поиск/косвенную ссылку.
Помимо создания кода, предназначенного для i386, для небольших операций, Psyco кеширует этот транслированный машинный код для дальнейшего использования. Если Psyco способен установить, что отдельная операция такая же, как выполненная (и "обработанная") ранее, он может положиться на этот кэшированный код, а не транслировать этот сегмент. Это экономит еще немного времени.
Реальная экономия в Psyco, однако, является результатом распределения операций по трем различным уровням. Для Psyco существуют переменные "времени исполнения ("run-time"), "времени трансляции" ("compile-time") и "виртуального времени" ("virtual-time"). При необходимости Psyco перемещает переменные с одного уровня на другой. Переменные времени исполнения - это просто исходный байткод и структуры объекта, которые обрабатывает обычный интерпретатор Python. Переменные времени трансляции отображаются в машинные регистры и ячейки памяти с прямым доступом, как только Psyco транслирует эти операции в машинный код.
Наиболее интересный уровень - это переменные виртуального времени. Изнутри переменная Python - это полная структура с множеством членов - даже если объект представляет всего лишь целое число. Переменные виртуального времени Psyco представляют объекты Python, которые потенциально могут быть построены, если возникнет такая необходимость, но их деталями пренебрегают, пока необходимости нет. Например, рассмотрим следующее присваивание:
x = 15 * (14 + (13 - (12 / 11)))
Стандартный Python строит и разрушает ряд объектов, чтобы вычислить это значение. Целый целочисленный (integer) объект создается для того, чтобы содержать значение (12/11); затем значение извлекается из структуры этого временного объекта и используется для вычисления нового временного объекта (13-PyInt). Psyco пропускает эти объекты и просто вычисляет значения, зная, что "при необходимости" объект может быть создан из этого значения.
Используя Psyco
Гораздо легче использовать Psyco, чем его объяснять. В основном, все, что нужно, - это сказать Psyco какую функцию/метод "обрабатывать". Не надо менять код ни одной из ваших функций Python, ни сами классы.
Существует пара подходов, предназначенных для указания, что Psyco должен делать. "Бронебойный" подход - это разрешить Psyco везде выполнять операцию компиляции по месту. Чтобы сделать это, поместите следующие строки кода в начале вашего модуля:
import psyco ; psyco.jit()
from psyco.classes import *
Первая строка указывает Psyco применить свою магию ко всем глобальным функциям. Вторая строка (для Python 2.2 и выше) приказывает Psyco сделать то же самое с методами класса. Чтобы немного точнее направить поведение Psyco, вы можете воспользоваться командой:
psyco.bind(somefunc) # or method, class
newname = psyco.proxy(func)
Вторая форма оставляет func в качестве стандартной функции Python, но оптимизирует вызовы, которые задействуют newname. Практически во всех случаях, кроме тестирования и отладки, форма psyco.bind() - это то, что вы будете использовать.
Производительность Psyco
Несмотря на магию Psyco, его использование требует некоторого осмысления и тестирования. Главное, что нужно понять - это то, что Psyco полезен для обработки многократно выполняющиехся циклов, и то, что он знает, как оптимизировать операции, в которых задействованы целые и числа с плавающей запятой. Для нециклических функций и для операций над другими типами объектов Psyco обычно просто добавляет накладные расходы на свой анализ и внутреннюю компиляцию. Более того, для приложений с большим числом функций и классов, включение Psyco для приложения целиком является дополнительным бременем при трансляции машинного кода и использование памяти для этого кэширования. Гораздо лучше выборочно подключить те функции, которые смогут извлечь больше пользы из оптимизации Psyco.
Я начал с самого простого тестирования. Я просто подумал, какое из приложений, которые я недавно запускал, неплохо было бы ускорить. Первый пример, который пришел мне в голову, - это программа манипулирования текстом, которую я использую для преобразования чернового варианта моей будущей книги "Текстовая обработка в Python" (Text Processing in Python) в формат LaTeX. Это приложение использует некоторые строчные методы, некоторые регулярные выражения и некую программную логику, управляемую главным образом регулярными выражениями и совпадениями строк. В действительности это ужасный кандидат для Psyco, но поскольку я его использую, я начал с него.
При первом заходе, все, что я сделал - это добавил psyco.jit() в начало моего скрипта. Достаточно безболезненно. К сожалению, результаты были (как и ожидалось) удручающими. Если первоначально скрипт работал 8.5 секунд, то после "ускорения" с Psyco он выполнялся 12 секунд. Скверно! Я догадался, что компиляция по месту, вероятно, имеет некоторые накладные расходы на запуск, которые затягивают время исполнения. Поэтому следующее, что я сделал, была обработка гораздо более крупного входного файла (состоящего из множества копий первоначального файла). Это привело к крайне скромному успеху - время выполнения сократилось со 120 секунд до 110. Это улучшение оставалось устойчивым при нескольких запусках, но в любом случае весьма незначительным.
Второй заход с моим кандидатом на обработку текста. Вместо добавления вызова psyco.jit() без параметров, я добавил только строку psyco.bind(main), поскольку функция main() имеет несколько циклов (но минимально использует арифметические операции с целыми числами). В этом случае результаты номинально были лучше. Этот подход урезал время выполнения на несколько десятых секунды для первого примера и на несколько секунд для версии большого входного файла. Но по-прежнему ничего впечатляющего (хотя и никакого вреда).
Для более достойного тестирования Psyco, я откопал некий код нейронной сети, о котором я писал в одной из предыдущих статье (см. Ресурсы). Это приложение "code_recognizer" может быть настроено для опознания возможных распределений разных значений ASCII в различных языках программирования. Нечто подобное потенциально могло бы быть полезно при угадывании типов файла (скажем, потерянных сетевых пакетов); но этот код в действительности полностью универсален в отношении того, чему он был обучен - с таким же успехом он мог бы научиться распознавать лица или звуки, или виды приливов. В любом случае "code_recognizer" базируется на библиотеке Python bpnn, которая в качестве контрольного примера также включена (в модифицированной форме) в дистрибутив Psyco 0.4. Что важно знать о "code_recognizer" в свете этой статьи, так то, что оно вычисляет много циклов с числами с плавающей запятой, и что его выполнение занимает много времени. Вот у нас и появился хороший кандидат для Psyco.
Поэкспериментировав немного, я детально установил, как использовать Psyco. Для этого приложения в случае небольшого количества классов и функций не имеет особого значения, используете вы направленное связывание или по месту. Но лучший результат - на несколько процентов - по-прежнему оказывается за выборочным связыванием классов, которые лучше всего оптимизируются. Более важно, однако, понять область связывания Psyco.
Скрипт "code_recognizer" содержит строки наподобие:
class NN2(NN):
# customized output methods, math core inherited
Другими словами, интересный момент с точки зрения Psyco находится в классе bpnn.NN. Добавление psyco.jit() или psyco.bind(NN2) в скрипт code_recognizer.py мало что дает. Чтобы Psyco выполнял желаемую оптимизацию, вам потребуется добавить psyco.bind(NN) в code_recognizer.py, либо psyco.jit() в bpnn.py. В отличие от того, что вы могли бы предположить, компиляция по месту происходит не при создании экземпляра или вызове методов, а при описании класса. Вдобавок, при подключении производных классов их унаследованные методы не обрабатываются.
Как только были выработаны детали подходящего связывания Psyco, результирующее ускорение оказалось весьма впечатляющим. Используя те же примеры тестирования и режим обучения сети, который был представлен в упомянутой статье (500 шаблонов обучения, 1000 итераций обучения), время обучения нейронной сети было уменьшено с где-то 2000 секунд до приблизительно 600 секунд - больше, чем в три раза. Сокращение числа итераций до 10 показало пропорциональное ускорение (но никуда не годное распознавание нейронной сети) - так же как и промежуточное число итераций.
Я нахожу весьма замечательным ускорение кода, выполнявшегося полчаса, до 10 минут, с помощью двух строк кода. Это ускорение, вероятно, все еще меньше скорости исполнения аналогичного приложения на С, и определенно меньше десятикратного ускорения, которое зарегистрировано в некоторых единичных случаях тестирования Psyco. Но это приложение явно из "реальной жизни", и эти улучшения достаточны для того, чтобы их отметить во многих контекстах.
Куда движется Psyco?
В настоящий момент Psyco не производит статистику или профилирование и выполняет только минимальную оптимизацию генерируемого машинного кода. Возможно, более поздняя версия будет знать, как определять операции, которые могли бы более всего выиграть от оптимизации, и выбрасывать из кеша машинный код для неоптимизируемых операций. Вдобавок, возможно, будущий Psyco мог бы решать выполнять более обширную (но и более дорогую) оптимизацию многократно исполняемых операций. Такой анализ исполнения был бы похож на то, что технология HotSpot компании Sun делает для Java. То обстоятельство, что Java, в отличие от Python, имеет декларации типов, в действительности менее значимо, чем думают многие (однако предшествующие работы по оптимизации Self, Smalltalk, Lisp и Scheme также отмечают это).
Хотя я и сомневаюсь, что это когда-нибудь случится, но было бы здорово, если бы технология типа Psyco была интегрирована в какую-нибудь будущую версию самого Python. Несколько строк для импорта и связываний - не так уж много работы, но тогда Python выполнялся бы существенно быстрее, и изначально это было бы гораздо более цельно. Поживём - увидим.