Пишемо код C на Cython

Останні два роки я вирішую всі завдання виключно на Cython. Це зовсім не означає, що я пишу на Пітоні, а потім «Ситонизирую» це з використанням різних типів декларацій, ні, я просто пишу на Cython. Я використовую «сирі» структури і масиви C (а іноді і вектори C++) і маленьку обгортку навколо malloc/free, яку я написав сам. Код працює практично так само швидко, як C/C++, тому що це і є код на C/C++, прикрашений синтаксичним цукром. Це код на C/C++ з функціоналом Python саме там, де мені це потрібно і де я цього хочу.

Фактично це протилежний варіант стандартного застосування мов, схожих з Python: ви пишете все додаток на Пітоні, оптимізуєте важливі місця на C і… Профіт! Швидкість C, зручність Пітона, вівці цілі, і вовки ситі.

В теорії це завжди виглядає краще, ніж на практиці. На практиці ваші структури даних впливають на ефективність вашого коду і трудомісткість його написання. Робота з масивами — це завжди біль, зате вони швидкі. Списки надзвичайно зручні, але дуже повільні. Цикли та виклики функцій у Пітоні завжди повільні, тому та частина програми, яку ви пишете C має тенденцію рости і рости до тих пір, поки практично всі ваш додаток не буде написано на C.

Нещодавно був опублікований пост про написання розширень C для Python. Автор написав реалізацію алгоритму на чистому Пітоні і на C, використовуючи Numpy C API. Я вирішив, що це гарна можливість продемонструвати відмінності, і, для порівняння, написав свій варіант на Cython:

import random
from cymem.cymem cimport Pool

from libc.math cimport sqrt

cimport cython

cdef struct Point:
double x
double y

cdef World class:
cdef Pool mem
cdef int N
cdef double* m
cdef Point* r
cdef Point* v
cdef Point* F
cdef readonly double dt
def __init__(self, N, threads=1, m_min=1, m_max=30.0, r_max=50.0, v_max=4.0, dt=1e-3):
self.mem = Pool()
self.N = N
self.m = <double*>self.mem.alloc(N, sizeof(double))
self.r = <Point*>self.mem.alloc(N, sizeof(Point))
self.v = <Point*>self.mem.alloc(N, sizeof(Point))
self.F = <Point*>self.mem.alloc(N, sizeof(Point))
for i in range(N):
self.m[i] = random.uniform(m_min, m_max)
self.r[i].x = random.uniform(-r_max, r_max)
self.r[i].y = random.uniform(-r_max, r_max)
self.v[i].x = random.uniform(-v_max, v_max)
self.v[i].y = random.uniform(-v_max, v_max)
self.F[i].x = 0
self.F[i].y = 0
self.dt = dt


@cython.cdivision(True)
def compute_F(World w):
"""Compute the force on each body in the world, w."""
cdef int i, j
cdef double s3, tmp
cdef Point s
cdef Point F
for i in range(w.N):
# Set all forces to zero. 
w.F[i].x = 0
w.F[i].y = 0
for j in range(i+1, w.N):
s.x = w.r[j].x - w.r[i].x
s.y = w.r[j].y - w.r[i].y

s3 = sqrt(s.x * s.x + s.y * s.y)
s3 *= s3 * s3;

tmp = w.m[i] * w.m[j] / s3
F. x = tmp * s.x
F. y = tmp * s.y

w.F[i].x += F. x
w.F[i].y += y F.

w.F[j].x -= F. x
w.F[j].y= y F.


@cython.cdivision(True)
def evolve(World w, int steps):
"""Evolve the world, w, through the given number of steps."""
cdef int _, i
for _ in range(steps):
compute_F(w)
for i in range(w.N):
w.v[i].x += w.F[i].x * w.dt / w.m[i]
w.v[i].y += w.F[i].y * w.dt / w.m[i]
w.r[i].x += w.v[i].x * w.dt
w.r[i].y += w.v[i].y * w.dt

Ця версія на Cython була написана за 30 хвилин, і вона така ж швидка, як код на C. Власне, чому б і ні, адже це і є код на C, просто написаний з застосуванням синтаксичного цукру. І вам навіть не потрібно думати про складному та ворожому C API і вивчати його, ви просто… просто пишете код C або C++. Обидві версії, C і Cython, приблизно в 70 разів швидше версії на чистому Пітоні, з урахуванням того, що вона використовує масиви Numpy.

Одне лише відміну від C: я використовую невелику обгортку для malloc/free, cymem, яку я написав сам. Все, що вона робить — це запам'ятовує адреси пам'яті, з якими працює, і звільняє непотрібну пам'ять коли спрацьовує збирач сміття. З тих пір, як я почав використовувати цю обгортку, у мене ніколи не було проблем з витоками пам'яті.

Проміжний варіант писати на Cython — використовувати typed memory-views, що дозволяє вам працювати з багатовимірними масивами Numpy. Однак для мене це виглядає більш складним. Зазвичай в своїх додатках я працюю з більш простими масивами, і волію визначати свої власні структури даних.

Перевів Dreadatour, текст читав %username%.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.