0,0
рейтинг
6 ноября 2012 в 18:06

Разработка → Производительность в Python. Легкий путь перевод tutorial

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

Программисты знакомые с ctypes врядли найдут тут что-то интересное, новичков же прошу под кат.

Ctypes — механизм Python для импорта функций из внешних библиотек.
%timeit — magic-функция оболочки IPython, измеряющая время выполнения выражения на Python


Ctypes — это прекрасно! Давайте начнем с небольшого банального примера: суммирование чисел в определенном диапазоне.
Вот реализация этой функции на Python
def sumrange(arg):
    return sum(xrange(arg))

Отлично! Но что если мы попробуем суммировать действительно большой диапазон чисел, например от 0 до 10**8 (т.е. 100,000,000)
In [2]: %timeit sumrange(10**2)
1000000 loops, best of 3: 1.53 us per loop

In [3]: %timeit sumrange(10**8)
1 loops, best of 3: 9.77 s per loop

In [4]: %timeit sumrange(10**9)
1 loops, best of 3: 97.8 s per loop

Уже не так весело. Попробуем кое-что другое:
def sumrange2(arg):
    x = i = 0
    while i < arg:
        x += i
        i += 1
    return x

Что из этого получится?
In [10]: %timeit sumrange2(10**2)
100000 loops, best of 3: 10.5 us per loop

In [11]: %timeit sumrange2(10**8)
1 loops, best of 3: 18.5 s per loop

Вот это да… Так еще хуже… В этот раз даже не буду пробовать 10**9.
Так как же нам ускорить выполнение? Только не предлагайте математические оптимизации… мы же в новом мире компьютеров! (в оригинале: don't suggest math tricks… this is the the new world of computing!)
Да, я знаю, что сложность алгоритма — постоянная величина и не зависит о величины аргумента, n*(n+1)/2. Но статья посвящена не этому.

Как насчет ctypes?

#include <stdio.h>

unsigned long long sumrange(unsigned long long arg)
{
    unsigned long long i, x;
    x = 0;

    for (i = 0; i < arg; i++) {
        x = x + i;
    }
    return x;
}

Сохраним с именем sumrange.c и скомпилируем (не будем использовать оптимизации для чистоты эксперимента):
$ gcc -shared -Wl,-install_name,sumrange.so -o sumrange.so -fPIC sumrange.c

Импортируем в Python то что получилось:
import ctypes
sumrange_ctypes = ctypes.CDLL('./sumrange.so').sumrange
sumrange_ctypes.restype = ctypes.c_ulonglong
sumrange_ctypes.argtypes = ctypes.c_ulonglong,


И Оскар получает…

In [15]: %timeit sumrange_ctypes(10**2)
1000000 loops, best of 3: 1.28 us per loop

In [16]: %timeit sumrange_ctypes(10**8)
1 loops, best of 3: 381 ms per loop

In [17]: %timeit sumrange_ctypes(10**9)
1 loops, best of 3: 3.79 s per loop

In [18]: %timeit sumrange_ctypes(10**10)
1 loops, best of 3: 37.8 s per loop


Итоговая сводка:
10**2 10**8 10**9 10**10
Чистый Python, способ №1 1.53 мкс 9.77 с 97.8 с -
Чистый Python, способ №2 10.5 мкс 18.5 с - -
ctypes 1.28 мкс 381 мс 3.79 с 37.8 с


Адский прирост производительности!

Для Node.js хакеров, есть эквивалент ctypes — FFI (Foreign Function Interface): github.com/rbranson/node-ffi
Перевод: James Robert
Alexander Efremov @siberianMan
карма
40,0
рейтинг 0,0
System Analyst
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Разработка

Комментарии (41)

  • +4
    Стоило бы еще упоминуть про cython: для арифметики он нормально заменяет ctypes и при этом более дружелюбный для тех кто не знает или не любит C. Кроме того он еще и более менее нормальные классы понятные для питона позволяет писать.

    немного найденого кода из pygame PaintBrush
    cimport cython
    import operator
    import math
    
    cdef extern from "math.h":
        double sqrt(double x)
        double atan2(double y, double x)
        double cos(double x)
        double sin(double x)
        double floor(double x)
    
    cdef class v2d(object):
        cdef public double x, y
        def __init__(self, x, y=None):
            if x is not None and y is not None:
                self.x = float(x)
                self.y = float(y)
            elif type(x) == v2d:
                self.x = x.x
                self.y = x.y
            elif type(x) == list or type(x) == tuple:
                self.x = x[0]
                self.y = x[1]
            else:
                raise TypeError()
        
        # Position
        def get_pos(self):
            return (self.x, self.y)
        def set_pos(self,pos):
            self.x = float(pos.x)
            self.y = float(pos.y)
        pos = property(get_pos, set_pos)
    
        def get_int_pos(self):
            cdef int x, y
            x = int(floor(self.x))
            y = int(floor(self.y))
            return(x,y)
        ipos = property(get_int_pos, set_pos)
    
    # ...
    


    А в некоторых случаях можно вообще писать чистый питон код и он его «ускорит».
    • 0
      >>А в некоторых случаях можно вообще писать чистый питон код и он его «ускорит».
      Пример пожалуйста, а то юзаю как плюсы так и питон. Но навскидку не могу привести пример Ваших «ускорит», а очень интересно!
      • +1
        Ну удачный случай я подобрать не смог, а все свои старые эксперименты я потерял.

        Вот файлы для проверки:
        gist.github.com/4030438

        imp@home ~/zz/cython $ python pyt.py 
        12.2809770107
        imp@home ~/zzz/cython $ python cyt.py
        10.8371829987
        


        Отрыв хоть и небольшой, но сохраняется постоянно. У них там есть какая-то магия с numpy — вроде всё что использует его может ускорится.

        Ну а в целом всё-равно нужно прибегать к дополнительной магии: если добавить еще один файл который подскажет cython`у где какие типы переменных, то отрыв сразу увеличится, а модуль при этом всё-равно можно будет импортировать как скомпилированный, так и не скомпилированный,

        Например в файл v2d.pxd добавим такое:
        cimport cython
        
        cdef class v2d(object):
            cdef public float __x, __y
        


        и запустим заново:
        imp@home ~/zzz/cython $ python cyt.py
        8.40173888206
        ...
        8.0
        


        И так можно дальше улучшать, например как в предыдущем комментарии взять функции из math.h и использовать их. Еще можно взять и переопределить функции:
        cimport cython
        
        cdef class v2d(object):
            cdef public float __x, __y
        
            cpdef float get_length(self)
        

        В данном случае это прироста в скорости не дало, может быть даже замедлило

        Еще можно обойтись и без создания pxd и писать прямо в .py файле сохраняя совместимость (см v2d_pure.py)
        В данном случае вс' стало даже медленнее.

        Реальные ускорения можно получить если поступать так:
        Делать большинство методов чисто сишнымии во внтутренних рассчётах использовать из, а наружу выставлять уже функции питона

        pyfile:
        @cython.cfunc
        @cython.returns(cython.float)
        @cython.locals(x=cython.float, y=cython.float)
        def __cget_length(self, x, y):
            pass
        
        @cython.cfunc
        @cython.returns(cython.float)
        @cython.locals(x=cython.float, y=cython.float, z=cython.float)
        def __ccomplex_calcs(self, x, y):
            z = self.__cget_length(x, y) ** 8 * self.__cget_anima()
        
        def get_length(self, x, y):
            return self.__cget_length(x, y)
        
        def complex_calcs(self, x,y):
            return self.__ccomplex_calcs( x, y)
        


        Мне этот «Pure» режим что-то выглядит стрёмно — pxd файл как-то попроще.

        *.pxd: +simpler, -harder to sync
        @cython.decorators: -huge, +always up to date

        Ну а вообще если хочется из него выжать всёи получить прирост в скорости десятки и сотни раз, то надо использовать сишные функции как в комментарии выше, писать сразу pyx файл, а не разбивать на py and pxd. В чем еще преимущество cython, так это в том, что на выходе можно получить библиотеку v2d.so и использовать её хоть прямиком из Си, так-что те кто си не любит может всё-равно выдавать для него либы
      • +1
        На прошлой работе использовали cython для обфускации кода.
        Кое-где делали жесткую обфускацию, а коли это почти бесплатно все остальные «core.py» в каждом модуле тоже тупо переименовывали в pyx и компилили.

        Факт фактом — почти любой питонячий код простая перекомпилляция в cython ускоряет)

        Не знаю как дело обстоит сейчас с python 3.3 :)
  • 0
    Питон берет скоростью разработки. Когда нужна производительность, пайтон — далеко не лучший вариант. Иногда получаешь граблями по лбу даже там, где совсем не ожидаешь.

    Вот к примеру, две одинаковые реализации флойда, тут и тут. Разницы вроде как и нет, а разница во времени выполнения ощутимая.
    • 0
      Это известный факт: глобальные переменные работают медленнее локальных.
      • –1
        Объясните новичку, почему так получается?
        • +7
          Для доступа к глобальной переменной используется поиск по hashtable (dict), локальные доступны просто по целочисленному индексу. Поиск по hashtable, понятное дело, гораздо дороже.
  • +7
    … и Оскар переходит к cython!

    Вот продолжение простенького бенчмарка. Код расположен в gist — https://gist.github.com/4025567. Запускаем!

    $ for power in 2 4 6 ; do for module in timing_{native,ctypes,cython,cython_range} ; do echo -n "$module: $power "; python2 -m timeit --setup "from $module import test" "test(10 ** $power)" ; done ; echo "---------" ; done
    timing_native: 2 100000 loops, best of 3: 4.72 usec per loop
    timing_ctypes: 2 100000 loops, best of 3: 2.27 usec per loop
    timing_cython: 2 1000000 loops, best of 3: 0.21 usec per loop
    timing_cython_range: 2 1000000 loops, best of 3: 0.211 usec per loop
    ---------
    timing_native: 4 1000 loops, best of 3: 448 usec per loop
    timing_ctypes: 4 10000 loops, best of 3: 30.1 usec per loop
    timing_cython: 4 100000 loops, best of 3: 5.57 usec per loop
    timing_cython_range: 4 100000 loops, best of 3: 6 usec per loop
    ---------
    timing_native: 6 10 loops, best of 3: 248 msec per loop
    timing_ctypes: 6 100 loops, best of 3: 2.59 msec per loop
    timing_cython: 6 1000 loops, best of 3: 541 usec per loop
    timing_cython_range: 6 1000 loops, best of 3: 586 usec per loop
    ---------
    


    Отмечу, что можно использовать даже родные циклы через range (если переменная-итератор — int, то он будет развернут в родной сишный).

    Не владею магией низкоуровнего питона, предполагаю, что у cython меньше оверхед на преобразование типов в сишные и обратно при вызове.
    • 0
      Ради интереса сравнивал скорость работы Delphi-проекта и python. В теле цикла сделал выражение со сложением, умножением и делением. Плюс периодичный вывод значений (раз в 10000 вроде бы, уже не помню). Результат тоже поразил: примерно в 100 раз делфи был быстрее. Далее написал библиотеку на CPython (функция-цикл вызывала переданную из python-кода функцию для вывода промежуточных результатов) и откомпилировал ее, подгрузил в Python-программу. В итоге ускорил цикл в 20 раз на windows и примерно в 15 на linux (почему такая разница получилась — не разбирался).
      • +1
        на живой программе с IO результаты сравнения будут совсем другими.
        А если просто «считать числа», то GPU (с тем же pyopencl) порвет всех как Тузик тряпку.
    • 0
      Я полагаю разница из-за того, что при компиляции ctypes не были включены оптимизации, а при компиляции cython было включено -O2.
      • +1
        ctypes очень медленный на переходе барьера C <-> Python, если что
        • +1
          Ну чтобы это продемонстрировать, нужно сравнить программы откомпилированные с одинаковыми ключами, верно?
          • 0
            Верно для рассматриваемого примера.
            Для оценки же тормозов вызова функций через ctypes нужно тестировать вызов пустой функции.
            Тормоза — одна из главных причин почему в CPython stdlibrary не используется ctypes.
            Вторая — ABI гораздо более изменчив чем API и поддерживать ABI совместимость заметно труднее.
            Недавно эта тема опять поднималась на python-dev: code.activestate.com/lists/python-dev/118511/
  • +1
    Ну и для тех, кто не хочет сильно думать о типах и вызове сей из питона, я упомяну про такую штуку как SWIG, который умеет генерировать обертки.
    • НЛО прилетело и опубликовало эту надпись здесь
      • 0
        Спасибо, это мне не попадалось. Хотя, справедливости ради, скажу, что у меня после некоторых плясок с бубном (а где их нет?) SWIG тихо мирно делает то, что я от него хочу без каких либо проблем.
        • НЛО прилетело и опубликовало эту надпись здесь
          • 0
            Ага, именно с плюсами он у меня и используется.
            • +1
              Для плюсов больший смысл имеет boost.python
              • 0
                к слову
                сэмпл используя boost::python (windows, boost 1.51, release)

                #include <boost/python.hpp>
                using namespace boost::python;
                
                namespace Test
                { 
                     unsigned long long sumrange(unsigned long long arg)
                     {
                          unsigned long long i, x;
                          x = 0;
                          for (i = 0; i < arg; i++) {
                               x = x + i;
                          }
                          return x;
                     }
                }
                
                
                BOOST_PYTHON_MODULE(boost_python_module)
                {
                     def("sumrange", Test::sumrange);
                }
                

                import timeit
                from boost_python_module import sumrange as sumrange2
                
                def sumrange1(arg):
                    return sum(xrange(arg))
                
                for power in (2, 8, 9):
                    loops = 10**power
                    t = timeit.Timer(lambda : sumrange1(loops))
                    print("Loops: {0} Time: {1}".format(loops, t.timeit(1)))
                print()
                for power in (2, 8, 9):
                    loops = 10**power
                    t = timeit.Timer(lambda : sumrange2(loops))
                    print("Loops: {0} Time: {1}".format(loops, t.timeit(1)))
                

                Результат:
                Loops: 100 Time: 8.08813168841e-06
                Loops: 100000000 Time: 14.5755965742
                Loops: 1000000000 Time: 143.893784101
                ()
                Loops: 100 Time: 2.51157773619e-05
                Loops: 100000000 Time: 0.173007690962
                Loops: 1000000000 Time: 1.69442229668
  • 0
    Не понятно только, в чём смысл не использовать оптимизацию. Ещё за компанию OpenMP / GPU /…
    • 0
      В оригинальной статье автор сперва использует оптимизацию, но в этом случае компилятор оптимизирует алгоритм так, что время выполнения становится постоянным независимо от аргумента. Я эту часть статьи просто пропустил
      Иначе говоря, оптимизации отключена, чтоб сравнивать реализацию одного и того же алгоритма.
      • 0
        Думаю, что автор оригинальной статьи схалтурил. Какой смысл бороться за производительность, и при этом выключать оптимизацию? Можно было сделать сумму немного сложнее, чтобы компилятор не суммировал арифметическую последовательность.
        • 0
          Просто автор оригинальной статьи подобрал не самый лучший синтетический пример.
          А автор перевода хотел заменить его чем-нибудь поудачнне, например поиском N-го простого числа, но постеснялся, т.к. это уже не было бы переводом, а писать авторские статьи, на темы в которых плохо разбираешься не позволяет совесть.
  • +3
    у меня сломался CTRL-F. Я не смог найти pypy
    • 0
      Это к чему?
      Сбываются мои худшие подозрения: все участники обсуждения, включая переводчика статьи, не имеют опыта использвания обсуждаемых инструментов в реальной работе?
      Или вы таки используете PyPy в production?
      • 0
        не знаю на какой вопрос отвечать сначала, и не знаю тянут ли на «реальную работу» мои последние 3 года, где 90% кода работает ежедневно под pypy.
        • 0
          Если вы последние три года используете PyPy на работе повсеместно — извиняюсь и снимаю шляпу.
          А что с оставшимися 10%? Какую версию pypy используете сейчас? С какой начали? Какие были неприятности и неожиданности? Баги?
          • 0
            > А что с оставшимися 10%?

            1. пара модулей, где pypy странно требует больше памяти, чем CPython (ведь обычно даже меньше), но возможно дело в этом: bugs.pypy.org/issue1282

            2. lxml падал в многопоточных приложениях (трудно воспроизводимо)

            это из последних лет, а это из грядущего:

            3. pyCUDA/Theano не собирается даже

            > Какую версию pypy используете сейчас?

            сборку двухнедельной давности из репозитория, где fijal допили последний принципиальный для меня issue по numpy

            > С какой начали?

            следил с 0.9х какой-то, а пользоваться, где-то между 1.2 и 1.3

            Неприятности были скорее лишь в несовместности с некими важными библиотеками использующих внешний С-код (в первую очередь numpy, lxml), но это уже уходит в прошлое.

            Багов как таковых не было: трансляция pypy из pypy — это очень неслабый тест, который стоит в основе каждой сборки.
            • 0
              Если не секрет, расскажите, чем занимаетесь?
              • 0
                краулинг + text-mining движок

                пока с одним приложением (для онлайн рекрутингового рынка), потом есть конкретные планы к другим областям применять.
    • 0
      Недавно попробовал на одном из алгоритмов pypy и был в детском восторге от производительности. Огорчило, что с numpy не удалось подружить – это печаль, конечно.
      • 0
        > Огорчило, что с numpy не удалось подружить – это печаль, конечно

        что конкретно не вышло?
        • 0
          Вспомнить бы)
          Так как numpy использовался мало – спокойно его выпилил.
          Может быть я уже подзабыл и на самом деле из сторонних библиотек у меня PyYAML не завёлся. Но сейчас уже, к сожалению, не вспомнить.
  • +3
    Откуда эти значение по оси x 10^2, 10^8, 10^9 вы собираетесь график на логарифмической шкале рисовать?
    97.8 с /10^9 = 97 наносекунд на одно число.
    3.79 с /10^9 = 4 наносекунды.

    Я бы не стал называть эти результаты катастрофой, потому что это интерпретируемый язык. Операции в тесте настолько просты, что они могут трансформироваться в простые asm инструкции и я не исключаю, что можно достичь даже 1 наносекунды. Я бы посмотрел на это даже с другой стороны, стандартный overhead операции 90 наносекунд, что совсем немного. Если вы не декодируете видео, то для сортировки 100 строк и вывода их на экран 1 000 000 операций в секунду вполне достаточно.
  • 0
    Use Numba, nigga: numba.pydata.org/
  • НЛО прилетело и опубликовало эту надпись здесь
  • НЛО прилетело и опубликовало эту надпись здесь
  • 0
    Если использовать boost::python, то сразу получите готовый модуль Python.
    Всё что останется — сделать import.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.