воскресенье, 22 июля 2018 г.

Ускоряем Python через cython


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

В python есть такая вещь как Type Hints. Мы просто можем указать типы данных для передаваемых значений в функции, а также типы возвращаемых значений.

def greeting(name: str) -> str:
    return 'Hello ' + name
Конечно, для интерпретатора, это мало, что значит. Основная идея была в улучшении подсказок в средах разработки, упрощенного анализа кода и поиска багов, может помочь вам писать код строже (потому что вы начнете соблюдать типы и думать о структуре кода, а не пихать все в одну переменную, если вы конечно так делали), так же это может помочь статическим анализаторам кода в нахождении проблем. Штука интересная, но не сильно помогающая в продакшене. Но на самом деле, теперь мы можем сделать очень интересные вещи для ускорения кода. И нам поможет Cython. В детали не будем залазить, возьмем простой пример показывающий общую суть.

def fun(x: float) -> float:
    return x*x-x

def integrate_f(a: float, b: float, N: int):
    s : float = 0
    dx : float = (b - a) / N
    for i in range(N):
        s += fun(a + i * dx)
    return s * dx
Вот пример нашего кода на питоне (прямо из документации). Проверяем:

begin = time.time()
print(tpy.integrate_f(1006, 2610, 100000000))
print(time.time() - begin)

Итог:
5584257516.163277
25.64701795578003

25 секунд, не очень радостное время. Однако, если мы переименуем файл в pyx и дополним тремя декораторами:

import cython

@cython.cfunc  # cdef functions are faster but not callable from python
def fun(x: float) -> float:
    return x*x-x

@cython.locals(i=cython.int, N=cython.int)  # better integration soon
@cython.cdivision(True)  # remove divide by zero protection
def integrate_f(a: float, b: float, N: int):
    s : float = 0
    dx : float = (b - a) / N
    for i in range(N):
        s += fun(a + i * dx)
    return s * dx

Как это добро компилировать смотрим в документации. Проверяем: 

import t (наш cython модуль)
import time

if __name__ == '__main__':
    begin = time.time()
    print(t.integrate_f(1006, 2610, 100000000))
    print(time.time() - begin)

Итог:
5584257516.166599
0.15540504455566406

Результат более, чем впечатляет. Затраченных усилий минимум и увеличение в поддержке кода не значительное. Конечно чем сложнее куски, тем сложнее будет соблюдать типизацию, некоторые вещи могут вообще не дать видимого прироста. Однако, если вы уперлись в Python, то можно попробовать использовать Cython для получения профита. В каждой конкретной задаче нужно смотреть оправданность усилий и полученного результата, но в целом профит может быть велик.

Для сравнения делаем в лоб одинаковое решение на go:

package main

import (
    "log"
    "time"
)

func fun(x float32) float32 {
    return x*x - x
}

func integrate_f(a float32, b float32, N int) float32 {
    s := float32(0.0)
    dx := (b - a) / float32(N)
    for i := 0; i < N; i++ {
        s += fun(a + float32(i)*dx)
    }
    return s * dx
}

func main() {
    start := time.Now()
    integrate_f(1006, 2610, 100000000)
    elapsed := time.Since(start)
    log.Printf("time %s", elapsed)
}


Получим 0,105165. В данном случаи выгрыш от cython не значителен. Я это к тому, что в каждой задаче хорошо свое решение и пытаться переписать все на go не решение всех проблем.