Programming: Python:
Используйте Psyco, и Python будет работать так же быстро, как и С
Автор: Дэвид Мертц (David Mertz) - нештатн. журналист Gnosis Software Inc.
Перевод: Intersoft Lab
Источники:
Обычно 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
выполнялся бы существенно быстрее, и изначально это было бы
гораздо более цельно. Поживём - увидим. и на ресурса SourceForge.
(Simplified Wrapper and Interface
Generator, SWIG) - очень широко, и, возможно, повсеместно,
используемое средство написания модулей С/С++ для Python и
других "скриптовых" языков.
Грэг Эуинг создал , который используется для написания модулей
расширения Python. Идея Pyrex - определить язык, который,
являясь очень близким к самому Python, создает смесь из
Python и типов данных С, но который в конечном итоге
преобразуется и транслируется в С-расширения Python.
Язык Vyper Джона Макса Сколлера (John Max Skaller's) был
задуман как улучшенный Python, реализованный на Ocaml.
Ожидалось, что будет получена трансляция в тот же самый
машинный код, который генерирует Ocaml и который, в общем,
сопоставим с быстродействием С. К сожалению, сейчас Vyper -
мертвый проект, а версия компилирования так и не была
получена. состоялось, когда этот проект еще
был живым (developerWorks, октябрь 2000).
Дэвид в соавторстве с Эндрю Блейзом (Andrew Blais)
написали статью (developerWorks, июль 2001). В
ней они привели код, который опирается на модуль Python
bpnn, созданный Нилом Шеменойером (Neil
Schemenauer). Эта статья использует код нейронной сети,
чтобы продемонстрировать возможности Psyco.
Ряд в зоне Linux developerWorks.
Об авторе
Дэвид Мертц не состоялся ни как охотник, ни как рыбак, ни
как пастух. В результате он пришел к критическому критицизму.
Возможно, завтра он займется чем-нибудь еще. Дэвид доступен по
адресу: mertz@gnosis.cx,
а жизнь его описана на .
Присылайте свои замечания и предложения касательно этой,
прошлых или будущих статей.
Оригинальный текст статьи можно посмотреть
здесь:
При перепечатке любого материала
с сайта, видимая ссылка на источник www.warayg.narod.ru
и все имена, ссылки авторов обязательны.
© 2005
|