HPUNIX Сайт о ОС и не только!

Тест производительности скриптов на Python

26 июля 2012 - unix
Тест производительности скриптов на Python

Если вы издавна читаете мой блог, то сможете держать в голове, как несколько раз я гласил о Python различные противные вещи, мол он неспешный и памяти много ест. При всем этом даже приводились разные пруфлинки.

Но, поправде, нехорошо судить о языке по тому, массивы какой вложенности он поддерживает, с какой скоростью он делает пустой цикл из 100 тыщ 500 итераций и тд. Нас же интересует, как он совладевает с типовыми задачками. Так что я решил провести свой маленькой опыт.

1. Что и как будем определять?

Меня сначала заинтересовывают две вещи — скорость скриптов и сколько они едят памяти. Формально говоря, сам по для себя язык только косвенно оказывает влияние на скорость написанных с его помощью программ, либо то, сколько памяти они потребляют.

Но, повторюсь, нас заинтересовывают типовые случаи. А какой интерпретатор обычно употребляют Python-программисты? Верно, CPython.

Вот его и будем тестировать.

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

К счастью, имеется обычная задачка, которая обычно решается при помощи скриптов и удовлетворяет обоим аспектам. Это — парсинг огромного файла при помощи постоянных выражений. И только попытайтесь сказать, что это — нетипичная задачка.

Она охрененно обычная!

И последнее. Допустим, замерили мы скорость работы программки, она равна X. Это много либо не много? Глядя с чем ассоциировать!

Ассоциировать Python-скрипты будем с программками на С++ и скриптами на Perl (todo: в последующий раз не запамятовать про PHP). Тестировать все будем в последующем окружении:

$ python --version
Python 2.7.1

$ perl --version
This is perl 5, version 12, subversion Три (v5.12.3) built for i386-freebsd-64int
[...]

$ gcc -v
[...]
gcc version 4.2.1 20 миллионов 70 тыщ семьсот девятнадцать [FreeBSD]

$ uname -a
FreeBSD example.ru 8.0-RELEASE-p2 FreeBSD 8.0-RELEASE-p2 #0: Tue Jan  5 16:02:27 UTC Две тыщи 10     root@i386-builder.daemonology.net:/usr/obj/usr/src/sys/GENERIC  i386

Начнем!

2. Замеряем время выполнения

Где взять большой файл, чтоб пропарсить его? Я использовал слитый из кэша Гугл дамп некогда известного, а сегодня закрывшегося форума. Спасти удалось не все, но для наших нужд — довольно.

После объединения всех сохраненных страничек в один файл, вышло около Триста Мб HTML-кода. Сверх сложную задачку ставить нет смысла, так что попробуем просто извлечь все ник-неймы форумчан.

Решение на Python:

#!/usr/bin/env python
import sys
Тест производительности скриптов на Python
import re

for line in sys.stdin:
  match = re.search('foobarbaz[^>]*>([a-z0-9_]+)</a></td>', line, re.I)
  if match:
    print match.group(1)

Тут я не случаем использую конструкцию «for line in sys.stdin» заместо открытия файла и предстоящего чтения из него. Как выяснилось, 2-ой вариант работает осязаемо медленней. Как следует, имеет место некоторый побочный эффект, а мы условились их избегать.

Аналогичный скрипт, использующий подготовительную компиляцию постоянного выражения:

#!/usr/bin/env python
import sys
import re

regEx = re.compile('foobarbaz[^>]*>([a-z0-9_]+)</a></td>', re.I)
for line in sys.stdin:
  match = re.search(regEx, line)
  if match:
    print match.group(1)

Создатели книжки Python в системном администрировании UNIX и Linux говорят, что 2-ой скрипт должен работать резвее первого. Проверим!

Решение на Perl:

#!/usr/bin/env perl
use strict;

while(my $line = <>) {
  chomp($line);
  if(my @nickList =
    $line =~ m{foobarbaz[^>]*>([a-z0-9_]+)</a></td>}i) {
    print join "\n", @nickList;
    print "\n";
  }
}

Код программки на C++ можно получить методом внесения простых конфигураций в код из последующего пт, так что тут его я не привожу.

Чтоб быть совсем-совсем беспристрастными, необходимо в общей трудности проверить целых 6 вариантов скрипта. Два приведенных Python-скрипта необходимо (1) просто запустить в интерпретаторе, (2) за ранее скомпилировать в файл .pyc и только позже запустить, также (3) конвертировать в программку на Си при помощи Cython, собрать, используя GCC, и запустить получившийся бинарник. Мы проверим все эти варианты!

Конвертировать .py в .pyc до боли просто:

python -m compileall .

С Cython чуточку труднее. Поначалу получаем код на Си:

cython python.py --embed -o python.c

Далее можно попробовать его скомпилировать:

gcc -O2 ./python.c -o ./c_python \
  -I/usr/local/include/python2.7 -L/usr/local/lib/ -lpython2.7

Вот только это не будет работать. Полезут ошибки:

./python.c: In function 'main':
./python.c:845: error: conflicting types for 'm'
./python.c:838: error: previous definition of 'm' was here
./python.c:866: warning: comparison between pointer and integer

Смотрим python.c:

Тест производительности скриптов на Python
#ifdef __FreeBSD__
    fp_except_t m;

    m = fpgetmask();
    fpsetmask(m & ~FP_X_OFL);
#endif

Неувязка в том, что другая переменная с именованием m объявлена несколькими строчками выше. Так что вторую m необходимо переименовать, после этого программка удачно соберется.

Все готово, сейчас можно замерять время. В юниксах для этой цели обычно употребляется утилита time (не путать со одноименной командой в bash!):

/usr/bin/time -l cat BIG_FILE.TXT | ./perl.pl > /dev/null

Строго говоря, тут замеряется время работы утилиты cat, а не нашего скрипта. Это нетрудно проверить, дописав в конец скрипта команду «sleep(5)». Но передать имя входного файла в качестве аргумента мы не можем в связи с описанной чуть повыше особенностью Пайтона.

Условия должны быть схожими для всех испытуемых!

Можно было бы использовать команду типа:

/usr/bin/time -l sh -c 'cat BIG_FILE.TXT | ./perl.pl > /dev/null'

… но тогда какую-то погрешность занесет утилита sh. К счастью, экспериментальным методом нетрудно проверить, что замеры с внедрением и без использования sh дают фактически схожие результаты.

Все! Хватит теории. Давайте поглядим на числа:

Сравнение скорости - Python vs Perl vs C++

Как и следовало ждать, резвее всего с задачей совладала программка на C++ (я именовал ее boost). За ней с маленьким отрывом идет скрипт на Perl. А вот скрипт на Пайтоне (python.py) оказался в целых Четыре раза медленней скрипта на Perl!

Что умопомрачительно, подготовительная компиляция постоянного выражения (программки с «comp_re» в имени) только замедляет работу скрипта. С чем это связано — не знаю. Так либо по другому, товарищи Ноа Гифт и Джереми М.Джонс на этот счет нас околпачили.

Как и на счет того, что Python прост в исследовании и интуитивно понятен даже тем, кто никогда на нем не писал. Я не так давно начал на нем писать, так что сможете мне поверить.

Подготовительная компиляция скрипта не ускоряет его выполнения. Ну это понятно — скрипты маленькие, так что их преобразование из текста во внутреннее представление интерпретатора и так проходит стремительно. Хоть не замедляет — и на том спасибо :)

А вот что вызвало у меня реальный шок — это скорость программ, приобретенных с помощью Cython (имена начинаются с «c_python»). Я не знаю, почему они такие неспешные. Может быть, в libpython предусмотрены какие-то дополнительные проверки входных характеристик, приводящие к тормозам.

Похоже, единственная настоящая полезность от Cython заключается в способности запускать Python-скрипты там, где не установлен интерпретатор. И не требуется высочайшая производительность. Зато читатели знают.

3. Замеряем объем потребляемой памяти

Кроме времени выполнения, утилита time, запущенная с ключом -l, выводит много увлекательной инфы. Практически она выводит всю структуру rusage. Кроме остального, мы можем выяснить, какой наибольший объем памяти потребляла программка.

Давайте малость изменим задачку. Сейчас необходимо не просто извлечь из файла все имена юзеров, да и избавиться от дублей. Другими словами, программка должна выводить перечень уникальных ник-неймов.

При помощи утилиты sort просто проверить, что в файле содержится всего около Три тыщи уникальных ников, так что можно смело хранить их в памяти. В прошлом пт мы узнали, что подготовительная компиляция постоянных выражений в Python только вредит, так что сейчас обойдемся без *comp_re* скриптов. Тем паче, что они не должны значительно оказывать влияние на потребление памяти.

Следует направить внимание, что «память, применяемая процессом» — штука очень относительная. К примеру, некие странички могут попасть в своп. Не считая того — часть памяти отводится под саму программку (секция text), часть — под стек, часть — под данные (секция data).

Что, если программка весит 20 Мб, но под данные выделяет только 100 Кб памяти? А если программка и данные совместно занимают 10 Кб, а под стек выделен Один Мб? Также в Python очень интенсивно употребляется разделяемая память — она считается либо нет? (Кстати, к вопросу о том, для чего мне этот ваш ассемблер, если я пишу на Python.)

Я собрался сделать две версии скриптов. 1-ая версия — обычная, хранящая имена юзеров в памяти и выводящая только уникальные ники. 2-ая версия выводит все логины (с повторами) в stdout, как в прошлом пт.

Для обоих вариантов замеряем наивысшую resident memory size и находим разность результатов. Эта разность принимается равной объему памяти, необходимому для хранения логинов.

Скрипт на Python:

#!/usr/bin/env python
import sys
import re

# Может быть, используя set заместо dict, будут получены другие результаты
# Сможете считать проверку этого своим домашним заданием
nicks = dict()

if len(sys.argv) < 2:
print "Usage: " + sys.argv[0] + " <filename>"
  sys.exit()

f = file(sys.argv[1])

while True:
  line = f.readline()
  if Нуль == len(line):
    break
  match = re.search('foobarbaz[^>]*>([a-z0-9_]+)</a></td>', line, re.I)
  if match:
    nicks[match.group(1)] = 1

f.close();

for currNick in nicks.keys():
  print currNick

Скрипт на Perl:

#!/usr/bin/env perl
use strict;

my %nicks;
while(my $line = <>) {
Тест производительности скриптов на Python
  chomp($line);
  if(my @nickList =
    $line =~ m{foobarbaz[^>]*>([a-z0-9_]+)</a></td>}i) {
    for(@nickList) {
      $nicks{$_} = 1;
    }
  }
}

print "$_\n" for(keys %nicks);

Программка на C++, обещанная еще в прошлом пт:

/* (c) Alexandr A Alexeev Две тыщи одиннадцать | http://eax.me/ */
Тест производительности скриптов на Python
#include <iostream>
#include <fstream>
#include <string>
#include <set> // <unordered_set>
#include <boost/regex.hpp>
using namespace std;

namespace std {
  namespace tr1 = ::boost;
}

int main(int argc, char** argv) {
  set<string> nicks;

  if(argc < 2) {
    cout << "Usage: " << argv[0] << " <infile>" << endl;
    return 1;

Урок 6. Простой пример написания python скрипта

  }
  ifstream infile;
  infile.open(argv[1]);
  if(!infile.is_open()) {
    cout << "Failed to open '" << argv[1] << "' for reading!"
         << endl;
    return 2;
  }

  string line;
  tr1::smatch match;
  tr1::regex re("foobarbaz[^>]*>([a-zA-Z0-9_]+)</a></td>");
  while(infile.good()) {
    getline(infile, line);
    if(tr1::regex_search(line, match, re)) {
      nicks.insert(match[1]);
    }
  }

  for(
    set<string>::iterator currNick = nicks.begin();
    currNick != nicks.end();
    currNick++) {
    cout << *currNick << endl;
  }

  return 0;
}

Тут мне следовало бы использовать unordered_set, оператор auto и интегрированные постоянные выражения C++0x. К огорчению, поддержки всего этого нет в GCC 4.2, а скачать компилятор посвежее я поленился.

Собирается программка последующей командой:

g++ -lboost_regex -O2 boost_u.cpp -o boost_u \
  -I/usr/local/include -L/usr/local/lib

Вы могли увидеть, что сейчас в Python-скрипте мы читаем данные из файла, а не из stdin, как делали это в пт 2. Это не случаем. Во-1-х, на данный момент нас не интересует скорость скрипта. Во-2-х, мы не можем определять характеристики утилиты cat, как делали это в прошедший раз.

Разумеется, что объем потребляемой ею памяти никак не связан с объемом памяти, применяемой скриптами. Другими словами, утилита time сейчас употребляется так:

/usr/bin/time -l ./perl_u.pl ./BIG_FILE.TXT > /dev/null

Были получены последующие результаты:

Сравнение потребления памяти - Python vs Perl vs C++

Программки с постфиксом «_u» (от «unique») хранят логины в памяти и выводят только уникальные имена.

Perl опять затмил Python, выделив под хранение данных около 70 Кб памяти. Пайтону в свою очередь пригодилось 80-95 Кб. А вот память, использованную программкой на C++, похоже, просто не удалось замерить!

Разумеется, что Три тыщи ников, имеющих среднюю длину Восемь знаков, никак не умещаются в Четыре Кб памяти. Даже если в GNU-версии STL контейнер set употребляет для хранения строк какое-то хитрое дерево знаков. Применение метода сжатия также маловероятно.

Вероятнее всего, программка в обоих случаях зарезервировала малость памяти на будущее, чтоб уменьшить число системных вызовов.

Любопытно, что в этом случае программки на C++ потребляли больше памяти, чем скрипты на Perl либо Python. Но не торопитесь ликовать — для более больших программ такового происходить не будет. Не считая того, можно поиграться с флагами компилятора, чтоб уменьшить объем применяемой памяти.

4. Выводы

Каждый способен сделать собственные выводы из этой заметки. Себе я сделал последующие:

  • Python — неспешный язык, потребляющий много памяти. Но стоит приглядеться повнимательнее к PyPy;
  • Подготовительная компиляция .py скрипта в .pyc не ускоряет скрипт, но позволяет интерпретатору резвее его пропарсить. При всем этом скрипт потребляет мало меньше оперативки (пристально смотрим последнюю картину);
  • Cython осязаемо тормозит программку, но позволяет запускать ее в среде без установленного интерпретатора Python. Объем потребляемой программкой памяти возрастает им некординально;
  • Переписав программку с Perl на C++, можно ускорить ее на 20-25%;
  • Переписав программку с Python на C++, можно ускорить ее в 5 раз;
  • Скрипт на Perl вправду просит в 2-3 раза больше памяти, чем подобная программка на C++. Если не ошибаюсь, конкретно это прямым текстом и написано в Learning Perl;
  • Скрипт на Perl имеет в 2-3 раза меньше строк когда, чем подобная программка на C++ и приблизительно столько же строк, сколько в аналогичном скрипте на Python;
  • Утилита time — неплохой, пригодный инструмент. И не надо извращаться с системными функциями, ассемблерной аннотацией rdtsc и т.д.. В ОС уже все предвидено.

Соглашаться с этими выводами либо нет — дело ваше.

5. Дополнительная инфа

Другие материалы, посвященные производительности Пайтона:

  • http://mocksoul.livejournal.com/5789.html — тест производительности Java, C++, Python, Perl и PHP;
  • http://www.vitalik.com.ua/rails/perl-python-ruby-php.html — сопоставление Perl, Python, Ruby и PHP;
  • http://begemotov.net/creator/programming/ozarenie-ili-piton-mat-ego/ — о количестве памяти, потребляемой Пайтоном;
  • http://nobu.aoizora.org/?p=1068 — особенность собирателя мусора, приводящая к нехватки памяти;
  • http://redplait.blogspot.com/2011/05/beautiful-code.html — критика Python;
  • http://redplait.blogspot.com/2011/08/python-3.html — и еще мало критики;
  • http://eax.me/python-benchmark/#comment-241744727 — и еще мало критики;
  • http://attractivechaos.wordpress.com/2011/…benchmark-analyses/ — очень увлекательное сопоставление производительности Си, Java, C#, D, Go, Perl, Python, Lua, R, JavaScript и Ruby;
  • http://speed.pypy.org/ — производительность PyPy по сопоставлению с CPython;
  • http://koldunov.net/?p=417 — метод ускорения скриптов на Python за счет использования оптимизированных модулей;
  • http://www.perl.com/pub/27 Июня 2001/ctoperl.html — здравые мысли на тему, почему не надо пробовать ускорить скриптовые языки методом их трансляции в код на C/C++;
  • http://www.scipy.org/PerformancePython — ускорение Python скриптов методом inline-использования языков Си и Fortran, также Pyrex (который сейчас Cython);
  • http://habrahabr.ru/blogs/python/124388/ — об оптимизации скриптов на Python, статья Владислава Степанова (написана после публикации этой заметки);
  • http://habrahabr.ru/blogs/python/124862/ — ускорение Python скриптов с внедрением Cython, статья Юрия Блинкова (написана после публикации этой заметки);

Дополнение: Относительно 2-ух последних статей. К моему величавому огорчению, в обоих случаях сравнивается скорость Python скрипта после оптимизации со скоростью такого же скрипта до оптимизации.

Допустим, скрипт стал в 500 раз резвее. Спрашивается — как это резвее/медлительнее по сопоставлению с реализацией на C++ либо Perl? Также ничего не говорится по поводу применяемых объемов памяти.

Дополнение: А еще не так давно я застукал BitTorrent-клиент Deluge (написанный угадайте-на-чем) за поеданием порядка Девятьсот Мб оперативки:

Deluge - потребление памяти

Неувязка проявляется только после долговременной работы Deluge. В связи с этим я подозреваю, что винить в прожорливости программки следует garbage collector, который, по всей видимости, не совладал со собственной работой.

Как обычно, любые комменты приветствуются. Только, пожалуйста, воздержитесь от криков типа «В реальных задачках Питон в 20 раз резвее этого вашего Пёрла, просто вы измерять не умеете» либо высокоумных фраз в стиле «На практике 90% времени программки работают с диском и сетью, а не обрабатывают данные, ну и память сегодня дешевая».

Сможете обосновать 1-ое — пишите статью. Последнее является слабеньким утешением для тех, кто занимается конкретно вычислениями.

Похожие статьи

  • Набор скриптов для анализа веб-сайта

    Решил поделиться своими старенькими наработками, которые я использовал около года вспять на одном из собственных блогов. Архив со всеми скриптами находится тут. Дальше по тексту я кратко расскажу о его...

  • Об использовании модульных тестов и TDD

    В протяжении последних нескольких месяцев я практиковал TDD. Сейчас я понимаю — эта техника (не путать технику и методологию!) работает. Ее просто использовать и она отлично решает полностью конкр...

  • Berp Восемь тыщ двести двенадцать достаточно увлекательный компилятор Python

    Не так давно я натолкнулся на один любознательный проект. Он именуется Berp и представляет собой транслятор скриптов на языке Python в программки на Haskell. Со стороны юзера Berp смотрится как интерпр...

  • Предпосылки, по которым мне нравится Haskell

    Последний гиковcкий выпуск Radio-T (номер 253) вышел на уникальность увлекательным. Речь зашла о Scala, рефакторинге, TDD, багтрекерах, и даже (наконец!) о моем возлюбленном Haskell. К огорчению, тема...

  • Эволюция применяемых языков программирования

    На написание этой статьи меня вдохновили заметки На чём пишете? Дениса Филонова и Эволюция применяемых языков Даркуса. В их создатели вспоминают, как они начинали программировать, какие языки прогр...

Теги: ос
Рейтинг: +1 Голосов: 220 2445 просмотров
Комментарии (0)

Нет комментариев. Ваш будет первым!

Найти на сайте: параметры поиска

Windows 7

Среда Windows 7 на первых порах кажется весьма непривычной для многих.

Windows 8

Если резюмировать все выступления Microsoft на конференции Build 2013.

Windows XP

Если Windows не может корректно завершить работу, в большинстве случаев это

Windows Vista

Если к вашему компьютеру подключено сразу несколько мониторов, и вы регулярно...