Optimalizace až na Cost

Naučíme se optimalizovat funkce. Začneme od čisté implementace v Pythonu, zkusíme vyřešit problém v NumPy. Poté si ukážeme, jak postupně přejít k jazykům nižší úrovně, jako je Fortran nebo C, a také si představíme balík Numba.

In [1]:
%pylab inline --no-import-all
from IPython.display import Image
Populating the interactive namespace from numpy and matplotlib

Základní koncepce optimalizace

Už víme, že Python je jazyk, ve kterém jde velice efektivně programovat. A díky balíkům, jako jsou NumPy, SciPy nebo SymPy jde velice rychle řešit různé vědecké úlohy. Je pochopitelné, že s takovouto mírou abstrakce může být výsledný program pomalejší, než kdyby byl dobře napsán v nějakém kompilovaném jazyce typu C/C++ nebo Fortran. Musíme si ovšem uvědomit, že efektivita programu se měří v celkovém čase stráveném na vývoji a běhu programu (pro daný soubor úloh). Schématicky můžeme znázornit závislost rychlosti běhu programu v závislosti na délce vývoje asi takto:

In [2]:
Image(filename='optimizing-what.png')
Out[2]:

Všimněte si například, že typicky existuje nějaký offset ve vývojovém čase, tj. trvá nám déle v nízkoúrovňovém jazyce, než vůbec dostaneme první výsledek. Potřeba optimalizace tedy silně závisí na objemu výpočtů, které budeme s daným programem řešit.

Toto ovšem neznamená, že pokud je objem velký, máme hned začít programovat v C nebo Fortranu. Za chvíli si ukážeme, jak optimalizaci řešit chytřeji a postupně. Empirické pravidlo říká, že 90 % výpočetního času zabere 10 % zdrojového kódu. Jedná se o konkrétní příklad obecného Paretova 80 / 20 principu. Je tedy vhodné nejprve těchto 10 % najít a poté je teprve začít optimalizovat. Python nám k tomuto účelu poskytuje velice mocné nástroje.

Profilování

Profilování je nástroj, který nám umožní najít kritická místa v našem programu, oněch 10 %, které stojí za to optimalizovat. Zkusme si to ukázat na jednoduchém příkladu.

In [3]:
def heavy_calc(X):
    Y = X.copy()
    for i in range(10):
        Y = Y**i
    return Y

def heavy_loop(inputs):
    res = []
    for X in inputs:
        res.append(heavy_calc(X))
    return res

def code_setup():
    from numpy.random import rand
    N = 20
    M = 1000
    print("Will generate {} random arrays".format(N))
    inputs = [rand(M, M) for n in range(N)]
    print("Will calculate now")
    result = heavy_loop(inputs)
    print("Finished calculation")

Python obsahuje dva základní mofuly pro profilování - profile a cProfile, z nichž ten druhý je rychlejší. Pomocí funkce run pustíme výpočet pod dohledem cProfile, výsledky uložíme do souboru.

In [4]:
import cProfile
cProfile.run('code_setup()', 'pstats')
Will generate 20 random arrays
Will calculate now
Finished calculation

Dále budeme potřebovat modul pstats, který nám umožní s výsledky pracovat. Použije k tomu třídu Stats.

In [5]:
from pstats import Stats
p = Stats('pstats')

print_stats nám zobrazí prvních n záznamů.

In [6]:
p.print_stats(10)
Wed Nov 28 22:09:56 2018    pstats

         196 function calls in 6.554 seconds

   Random listing order was used
   List reduced from 27 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        9    0.000    0.000    0.000    0.000 {method 'append' of 'collections.deque' objects}
        6    0.000    0.000    0.000    0.000 {built-in method posix.getpid}
        9    0.000    0.000    0.000    0.000 {method 'acquire' of '_thread.lock' objects}
        1    0.000    0.000    6.554    6.554 {built-in method builtins.exec}
        2    0.000    0.000    0.000    0.000 {built-in method builtins.hasattr}
        7    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}
        3    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        1    0.000    0.000    0.000    0.000 {method 'format' of 'str' objects}
       20    0.000    0.000    0.000    0.000 {method 'append' of 'list' objects}
        1    0.000    0.000    6.531    6.531 <ipython-input-3-947eac7c0bd6>:13(code_setup)


Out[6]:
<pstats.Stats at 0x7f2f958ba828>

Ty jsou ovšem nesetříděné. Následující výstup už je užitečnější, záznamy jsou totiž setříděné podle celkového času stráveného v dané funkci. Navíc strip_dirs odstraní adresáře ze jmen funkcí.

In [7]:
p.strip_dirs().sort_stats('cumulative').print_stats(10)
Wed Nov 28 22:09:56 2018    pstats

         196 function calls in 6.554 seconds

   Ordered by: cumulative time
   List reduced from 27 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    6.554    6.554 {built-in method builtins.exec}
        1    0.023    0.023    6.554    6.554 <string>:1(<module>)
        1    0.000    0.000    6.531    6.531 <ipython-input-3-947eac7c0bd6>:13(code_setup)
        1    0.000    0.000    6.274    6.274 <ipython-input-3-947eac7c0bd6>:7(heavy_loop)
       20    6.210    0.311    6.274    0.314 <ipython-input-3-947eac7c0bd6>:1(heavy_calc)
        1    0.000    0.000    0.257    0.257 <ipython-input-3-947eac7c0bd6>:18(<listcomp>)
       20    0.257    0.013    0.257    0.013 {method 'rand' of 'mtrand.RandomState' objects}
       20    0.063    0.003    0.063    0.003 {method 'copy' of 'numpy.ndarray' objects}
        3    0.000    0.000    0.000    0.000 {built-in method builtins.print}
        6    0.000    0.000    0.000    0.000 iostream.py:366(write)


Out[7]:
<pstats.Stats at 0x7f2f958ba828>

Takto vypadá výstup setříděný pomocí nekumulovaného času.

In [8]:
p.sort_stats('time').print_stats(10)
Wed Nov 28 22:09:56 2018    pstats

         196 function calls in 6.554 seconds

   Ordered by: internal time
   List reduced from 27 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
       20    6.210    0.311    6.274    0.314 <ipython-input-3-947eac7c0bd6>:1(heavy_calc)
       20    0.257    0.013    0.257    0.013 {method 'rand' of 'mtrand.RandomState' objects}
       20    0.063    0.003    0.063    0.003 {method 'copy' of 'numpy.ndarray' objects}
        1    0.023    0.023    6.554    6.554 <string>:1(<module>)
        9    0.000    0.000    0.000    0.000 socket.py:333(send)
        1    0.000    0.000    6.274    6.274 <ipython-input-3-947eac7c0bd6>:7(heavy_loop)
        9    0.000    0.000    0.000    0.000 iostream.py:195(schedule)
        1    0.000    0.000    0.257    0.257 <ipython-input-3-947eac7c0bd6>:18(<listcomp>)
        6    0.000    0.000    0.000    0.000 iostream.py:366(write)
        1    0.000    0.000    6.554    6.554 {built-in method builtins.exec}


Out[8]:
<pstats.Stats at 0x7f2f958ba828>

Jupyter nám může usnadnit práci pomocí %prun a %%prun. Např.

In [9]:
%prun -s cumulative -l 10 code_setup()
Will generate 20 random arrays
Will calculate now
Finished calculation
 
         196 function calls in 6.478 seconds

   Ordered by: cumulative time
   List reduced from 27 to 10 due to restriction <10>

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        1    0.000    0.000    6.478    6.478 {built-in method builtins.exec}
        1    0.023    0.023    6.478    6.478 <string>:1(<module>)
        1    0.000    0.000    6.455    6.455 <ipython-input-3-947eac7c0bd6>:13(code_setup)
        1    0.000    0.000    6.175    6.175 <ipython-input-3-947eac7c0bd6>:7(heavy_loop)
       20    6.138    0.307    6.175    0.309 <ipython-input-3-947eac7c0bd6>:1(heavy_calc)
        1    0.000    0.000    0.279    0.279 <ipython-input-3-947eac7c0bd6>:18(<listcomp>)
       20    0.279    0.014    0.279    0.014 {method 'rand' of 'mtrand.RandomState' objects}
       20    0.037    0.002    0.037    0.002 {method 'copy' of 'numpy.ndarray' objects}
        3    0.000    0.000    0.001    0.000 {built-in method builtins.print}
        6    0.000    0.000    0.001    0.000 iostream.py:366(write)

Z obou výstupů celkem jasně vydíme, že naprostou většinu času trávíme ve funkci heavy_calc. Pokud se tedy chceme pustit do optimalizace, musíme se zaměřit právě na tuto část našeho programu.

Výsledky můžete navíc spojit s nástroji pro vizualizaci, např.SnakeViz nebo vprof.

Vzorová úloha - vzdálenost množiny bodů ve vícerozměrném prostoru

(Tento příklad byl převzat z http://jakevdp.github.io/blog/2013/06/15/numba-vs-cython-take-2.)

Zadání je jednoduché: pro M bodů v N rozměrném prostoru spočítejte vzájemnou vzdálenost $d$, která je pro dva body $x,y$ definovaná jako $\sqrt {\sum_{i=1}^N {{{\left( {{x_i} - {y_i}} \right)}^2}} } $. Výslekem je tedy (symetrická) matice $M\times M$.

In [10]:
# toto nechť jsou naše vstupní data
M = 1000
N = 3
X = np.random.random((M, N))

Implementace v čistém Pythonu

Nemůžeme asi očekávat, že toto bude nejrychlejší a nejsnadnější verze našeho programu. Přesto stojí za to ji vyzkoušet, navíc ji budeme ještě potřebovat.

In [11]:
def pairwise_python(X):
    M = X.shape[0]
    N = X.shape[1]
    D = np.empty((M, M), dtype=np.float)
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = np.sqrt(d)
    return D

Tahle funkce nám bude pomáhat ukládat výsledné časy z %timeit.

Do pairwise_times si uložíme výsledné časy.

In [12]:
import collections
pairwise_times = collections.OrderedDict()
In [13]:
timings = %timeit -o pairwise_python(X)
pairwise_times['plain_python'] = timings
4.78 s ± 66.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

To samé pomocí NumPy

V případě NumPy můžeme v tomto případě využít broadcasting. Celá funkce tak zabere jeden rádek.

In [14]:
def pairwise_numpy(X):
    return np.sqrt(((X[:, np.newaxis, :] - X) ** 2).sum(-1))

Zkusíme, jestli výsledky jsou stejné pomocí assert a numpy.all.

In [15]:
assert np.all(pairwise_numpy(X) == pairwise_python(X))
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-15-b8011b9037e0> in <module>()
----> 1 assert np.all(pairwise_numpy(X) == pairwise_python(X))

AssertionError: 

Nejsou - máme to špatně! Nebo možná jen trochu nepřesně? To ověříme pomocí numpy.allclose.

In [16]:
assert np.allclose(pairwise_numpy(X), pairwise_python(X), rtol=1e-10, atol=1e-15)

Výsledky jsou stejné až na velmi malé rozdíly - to je nebezpečí numerických výpočtů s konečnou přesností.

In [17]:
timings = %timeit -o pairwise_numpy(X)
pairwise_times['numpy'] = timings
38.8 ms ± 221 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

Vidíme, že jsme zkrátili běh programu více než 100-krát. To není špatné, navíc je implementace daleko jednodušší.

Přichází Cython

Cython je nástroj, který z Python programu, obohaceného o nějaké Cython direktivy, vytvoří program v C (případně C++), který je možné zkompilovat a okamžitě použít jako modul v Pythonu. Typickým příkladem Cython direktiv jsou statické typy. Cython samozřejmě umožňuje používat funkce z binárních knihoven s C rozhraním.

Zkusíme optimalizovat naší funkci pairwise_python.

  • Cython zdroják má koncovku .pyx (za začátku byl Pyrex).
  • Cython dokáže přeložit jakýkoli Python. Výsledkem je ale minimální (nebo spíš žádná) optimalizace.
  • cimport je analogie import, pracuje ale s Cython definicemi funkcí (.pxd soubory).
  • Cython dodává numpy.pyx, obsahující dodatečné informace pro kompilace NumPy modulů. Proto voláme cimport numpy.
  • Podobně libc je speciální modul Cythonu.

  • Funkce se deklarují (moho deklarovat) se statickými typy vstupních parametrů. My použijeme np.ndarray[np.float64_t, ndim=2].

  • Proměnné se deklarují pomocí cdef.
In [18]:
%%file cyfuncs.pyx

import numpy as np
# numpy pro Cython
cimport numpy as np
from libc.math cimport sqrt

# tohle je čistý Python
def pairwise0(X):
    M = X.shape[0]
    N = X.shape[1]
    D = np.empty((M, M), dtype=np.float)
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = np.sqrt(d)
    return D

# tady už začínáme optimalizovat, změny ale nejsou drastické
def pairwise1(np.ndarray[np.float64_t, ndim=2] X):
    cdef int M = X.shape[0]
    cdef int N = X.shape[1]
    cdef double tmp, d
    cdef np.ndarray D = np.empty((M, M), dtype=np.float64)
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = sqrt(d)
    return D
Overwriting cyfuncs.pyx
In [19]:
%%file setup.py

from distutils.core import setup
from Cython.Build import cythonize
import numpy

setup(
  name='cyfuncs',
  include_dirs=[numpy.get_include()],
  ext_modules=cythonize("cyfuncs.pyx"),
)
Overwriting setup.py
In [20]:
!python setup.py build_ext --inplace
Compiling cyfuncs.pyx because it changed.
[1/1] Cythonizing cyfuncs.pyx
running build_ext
building 'cyfuncs' extension
gcc -pthread -B /home/kuba/opt/conda/envs/fjfi/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include -I/home/kuba/opt/conda/envs/fjfi/include/python3.6m -c cyfuncs.c -o build/temp.linux-x86_64-3.6/cyfuncs.o
In file included from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/ndarraytypes.h:1818:0,
                 from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/ndarrayobject.h:18,
                 from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/arrayobject.h:4,
                 from cyfuncs.c:580:
/home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:15:2: warning: #warning "Using deprecated NumPy API, disable it by " "#defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
 #warning "Using deprecated NumPy API, disable it by " \
  ^~~~~~~
gcc -pthread -shared -B /home/kuba/opt/conda/envs/fjfi/compiler_compat -L/home/kuba/opt/conda/envs/fjfi/lib -Wl,-rpath=/home/kuba/opt/conda/envs/fjfi/lib -Wl,--no-as-needed -Wl,--sysroot=/ build/temp.linux-x86_64-3.6/cyfuncs.o -o /home/kuba/workspace/python_fjfi-git/numerical_python_course/lecture_notes.cz/cyfuncs.cpython-36m-x86_64-linux-gnu.so

Jak jsme již říkali, Cython vytvoří C zdroják, který se pak kompiluje pomocí běžného překladače (např. gcc). Pojďme se na tento soubor podívat.

In [21]:
from IPython.display import FileLink
FileLink('cyfuncs.c')
Out[21]:

Ten soubor je dlouhý ... Obsahuje spoustu Python "balastu", na kterém vidíme, jak je vlastně samotný CPython naprogramován. Naštěstí tento soubor obsahuje i komentáře, které říkají, které řádce daný blok odpovídá. Např.

 /* "cyfuncs.pyx":16
  *                 tmp = X[i, k] - X[j, k]
  *                 d += tmp * tmp
  *             D[i, j] = np.sqrt(d)             # <<<<<<<<<<<<<<
  *     return D
  * 
  */
In [22]:
import cyfuncs
In [23]:
print("cyfuncs obsahuje: " + ", ".join((d for d in dir(cyfuncs) if not d.startswith("_"))))
cyfuncs obsahuje: np, pairwise0, pairwise1

Podívejme se, jestli dostávám stále stejné výsledky.

In [24]:
assert np.allclose(pairwise_numpy(X), cyfuncs.pairwise1(X), rtol=1e-10, atol=1e-15)

No a jak jsme na tom s časem?

In [25]:
timings = %timeit -o cyfuncs.pairwise0(X)
pairwise_times['cython0'] = timings
4.6 s ± 73.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
In [26]:
timings = %timeit -o cyfuncs.pairwise1(X)
pairwise_times['cython1'] = timings
165 ms ± 2.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
IPython %%cython magic

IPython, tak jako v mnoha jiných případech, nám práci s Cythonem usnadňuje pomocí triku %%cython. Zkusíme ho použít. Zároveň zkusíme ještě více náš kód optimalizovat, zatím je totiž pomalejší než numpy.

In [27]:
%load_ext Cython
In [28]:
%%cython -a

import numpy as np
cimport numpy as np
cimport cython
from libc.math cimport sqrt

@cython.boundscheck(False)
@cython.wraparound(False)
def pairwise_cython(double[:, ::1] X):
    cdef int M = X.shape[0]
    cdef int N = X.shape[1]
    cdef double tmp, d
    cdef double[:, ::1] D = np.empty((M, M), dtype=np.float64)
    for i in range(M):
        for j in range(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = sqrt(d)
    return np.asarray(D)
Out[28]:
Cython: _cython_magic_0c93d885fa5495f86348e5c0493f4980.pyx

Generated by Cython 0.28.3

Yellow lines hint at Python interaction.
Click on a line that starts with a "+" to see the C code that Cython generated for it.

 01: 
+02: import numpy as np
  __pyx_t_1 = __Pyx_Import(__pyx_n_s_numpy, 0, 0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  if (PyDict_SetItem(__pyx_d, __pyx_n_s_np, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
/* … */
  __pyx_t_1 = __Pyx_PyDict_NewPresized(0); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 2, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  if (PyDict_SetItem(__pyx_d, __pyx_n_s_test, __pyx_t_1) < 0) __PYX_ERR(0, 2, __pyx_L1_error)
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
 03: cimport numpy as np
 04: cimport cython
 05: from libc.math cimport sqrt
 06: 
 07: @cython.boundscheck(False)
 08: @cython.wraparound(False)
+09: def pairwise_cython(double[:, ::1] X):
/* Python wrapper */
static PyObject *__pyx_pw_46_cython_magic_0c93d885fa5495f86348e5c0493f4980_1pairwise_cython(PyObject *__pyx_self, PyObject *__pyx_arg_X); /*proto*/
static PyMethodDef __pyx_mdef_46_cython_magic_0c93d885fa5495f86348e5c0493f4980_1pairwise_cython = {"pairwise_cython", (PyCFunction)__pyx_pw_46_cython_magic_0c93d885fa5495f86348e5c0493f4980_1pairwise_cython, METH_O, 0};
static PyObject *__pyx_pw_46_cython_magic_0c93d885fa5495f86348e5c0493f4980_1pairwise_cython(PyObject *__pyx_self, PyObject *__pyx_arg_X) {
  __Pyx_memviewslice __pyx_v_X = { 0, 0, { 0 }, { 0 }, { 0 } };
  PyObject *__pyx_r = 0;
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("pairwise_cython (wrapper)", 0);
  assert(__pyx_arg_X); {
    __pyx_v_X = __Pyx_PyObject_to_MemoryviewSlice_d_dc_double(__pyx_arg_X, PyBUF_WRITABLE); if (unlikely(!__pyx_v_X.memview)) __PYX_ERR(0, 9, __pyx_L3_error)
  }
  goto __pyx_L4_argument_unpacking_done;
  __pyx_L3_error:;
  __Pyx_AddTraceback("_cython_magic_0c93d885fa5495f86348e5c0493f4980.pairwise_cython", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __Pyx_RefNannyFinishContext();
  return NULL;
  __pyx_L4_argument_unpacking_done:;
  __pyx_r = __pyx_pf_46_cython_magic_0c93d885fa5495f86348e5c0493f4980_pairwise_cython(__pyx_self, __pyx_v_X);

  /* function exit code */
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}

static PyObject *__pyx_pf_46_cython_magic_0c93d885fa5495f86348e5c0493f4980_pairwise_cython(CYTHON_UNUSED PyObject *__pyx_self, __Pyx_memviewslice __pyx_v_X) {
  int __pyx_v_M;
  int __pyx_v_N;
  double __pyx_v_tmp;
  double __pyx_v_d;
  __Pyx_memviewslice __pyx_v_D = { 0, 0, { 0 }, { 0 }, { 0 } };
  int __pyx_v_i;
  int __pyx_v_j;
  int __pyx_v_k;
  PyObject *__pyx_r = NULL;
  __Pyx_RefNannyDeclarations
  __Pyx_RefNannySetupContext("pairwise_cython", 0);
/* … */
  /* function exit code */
  __pyx_L1_error:;
  __Pyx_XDECREF(__pyx_t_1);
  __Pyx_XDECREF(__pyx_t_2);
  __Pyx_XDECREF(__pyx_t_3);
  __Pyx_XDECREF(__pyx_t_4);
  __Pyx_XDECREF(__pyx_t_5);
  __PYX_XDEC_MEMVIEW(&__pyx_t_6, 1);
  __Pyx_AddTraceback("_cython_magic_0c93d885fa5495f86348e5c0493f4980.pairwise_cython", __pyx_clineno, __pyx_lineno, __pyx_filename);
  __pyx_r = NULL;
  __pyx_L0:;
  __PYX_XDEC_MEMVIEW(&__pyx_v_X, 1);
  __PYX_XDEC_MEMVIEW(&__pyx_v_D, 1);
  __Pyx_XGIVEREF(__pyx_r);
  __Pyx_RefNannyFinishContext();
  return __pyx_r;
}
/* … */
  __pyx_tuple__31 = PyTuple_Pack(10, __pyx_n_s_X, __pyx_n_s_X, __pyx_n_s_M, __pyx_n_s_N, __pyx_n_s_tmp, __pyx_n_s_d, __pyx_n_s_D, __pyx_n_s_i, __pyx_n_s_j, __pyx_n_s_k); if (unlikely(!__pyx_tuple__31)) __PYX_ERR(0, 9, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_tuple__31);
  __Pyx_GIVEREF(__pyx_tuple__31);
/* … */
  __pyx_t_1 = PyCFunction_NewEx(&__pyx_mdef_46_cython_magic_0c93d885fa5495f86348e5c0493f4980_1pairwise_cython, NULL, __pyx_n_s_cython_magic_0c93d885fa5495f863); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 9, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  if (PyDict_SetItem(__pyx_d, __pyx_n_s_pairwise_cython, __pyx_t_1) < 0) __PYX_ERR(0, 9, __pyx_L1_error)
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
  __pyx_codeobj__32 = (PyObject*)__Pyx_PyCode_New(1, 0, 10, 0, CO_OPTIMIZED|CO_NEWLOCALS, __pyx_empty_bytes, __pyx_empty_tuple, __pyx_empty_tuple, __pyx_tuple__31, __pyx_empty_tuple, __pyx_empty_tuple, __pyx_kp_s_home_kuba_cache_ipython_cython, __pyx_n_s_pairwise_cython, 9, __pyx_empty_bytes); if (unlikely(!__pyx_codeobj__32)) __PYX_ERR(0, 9, __pyx_L1_error)
+10:     cdef int M = X.shape[0]
  __pyx_v_M = (__pyx_v_X.shape[0]);
+11:     cdef int N = X.shape[1]
  __pyx_v_N = (__pyx_v_X.shape[1]);
 12:     cdef double tmp, d
+13:     cdef double[:, ::1] D = np.empty((M, M), dtype=np.float64)
  __pyx_t_1 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  __pyx_t_2 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_empty); if (unlikely(!__pyx_t_2)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_2);
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
  __pyx_t_1 = __Pyx_PyInt_From_int(__pyx_v_M); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  __pyx_t_3 = __Pyx_PyInt_From_int(__pyx_v_M); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_3);
  __pyx_t_4 = PyTuple_New(2); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_4);
  __Pyx_GIVEREF(__pyx_t_1);
  PyTuple_SET_ITEM(__pyx_t_4, 0, __pyx_t_1);
  __Pyx_GIVEREF(__pyx_t_3);
  PyTuple_SET_ITEM(__pyx_t_4, 1, __pyx_t_3);
  __pyx_t_1 = 0;
  __pyx_t_3 = 0;
  __pyx_t_3 = PyTuple_New(1); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_3);
  __Pyx_GIVEREF(__pyx_t_4);
  PyTuple_SET_ITEM(__pyx_t_3, 0, __pyx_t_4);
  __pyx_t_4 = 0;
  __pyx_t_4 = __Pyx_PyDict_NewPresized(1); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_4);
  __pyx_t_1 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_1);
  __pyx_t_5 = __Pyx_PyObject_GetAttrStr(__pyx_t_1, __pyx_n_s_float64); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_5);
  __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
  if (PyDict_SetItem(__pyx_t_4, __pyx_n_s_dtype, __pyx_t_5) < 0) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;
  __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_2, __pyx_t_3, __pyx_t_4); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_5);
  __Pyx_DECREF(__pyx_t_2); __pyx_t_2 = 0;
  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
  __pyx_t_6 = __Pyx_PyObject_to_MemoryviewSlice_d_dc_double(__pyx_t_5, PyBUF_WRITABLE); if (unlikely(!__pyx_t_6.memview)) __PYX_ERR(0, 13, __pyx_L1_error)
  __Pyx_DECREF(__pyx_t_5); __pyx_t_5 = 0;
  __pyx_v_D = __pyx_t_6;
  __pyx_t_6.memview = NULL;
  __pyx_t_6.data = NULL;
+14:     for i in range(M):
  __pyx_t_7 = __pyx_v_M;
  __pyx_t_8 = __pyx_t_7;
  for (__pyx_t_9 = 0; __pyx_t_9 < __pyx_t_8; __pyx_t_9+=1) {
    __pyx_v_i = __pyx_t_9;
+15:         for j in range(M):
    __pyx_t_10 = __pyx_v_M;
    __pyx_t_11 = __pyx_t_10;
    for (__pyx_t_12 = 0; __pyx_t_12 < __pyx_t_11; __pyx_t_12+=1) {
      __pyx_v_j = __pyx_t_12;
+16:             d = 0.0
      __pyx_v_d = 0.0;
+17:             for k in range(N):
      __pyx_t_13 = __pyx_v_N;
      __pyx_t_14 = __pyx_t_13;
      for (__pyx_t_15 = 0; __pyx_t_15 < __pyx_t_14; __pyx_t_15+=1) {
        __pyx_v_k = __pyx_t_15;
+18:                 tmp = X[i, k] - X[j, k]
        __pyx_t_16 = __pyx_v_i;
        __pyx_t_17 = __pyx_v_k;
        __pyx_t_18 = __pyx_v_j;
        __pyx_t_19 = __pyx_v_k;
        __pyx_v_tmp = ((*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_X.data + __pyx_t_16 * __pyx_v_X.strides[0]) )) + __pyx_t_17)) ))) - (*((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_X.data + __pyx_t_18 * __pyx_v_X.strides[0]) )) + __pyx_t_19)) ))));
+19:                 d += tmp * tmp
        __pyx_v_d = (__pyx_v_d + (__pyx_v_tmp * __pyx_v_tmp));
      }
+20:             D[i, j] = sqrt(d)
      __pyx_t_20 = __pyx_v_i;
      __pyx_t_21 = __pyx_v_j;
      *((double *) ( /* dim=1 */ ((char *) (((double *) ( /* dim=0 */ (__pyx_v_D.data + __pyx_t_20 * __pyx_v_D.strides[0]) )) + __pyx_t_21)) )) = sqrt(__pyx_v_d);
    }
  }
+21:     return np.asarray(D)
  __Pyx_XDECREF(__pyx_r);
  __pyx_t_4 = __Pyx_GetModuleGlobalName(__pyx_n_s_np); if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 21, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_4);
  __pyx_t_3 = __Pyx_PyObject_GetAttrStr(__pyx_t_4, __pyx_n_s_asarray); if (unlikely(!__pyx_t_3)) __PYX_ERR(0, 21, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_3);
  __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
  __pyx_t_4 = __pyx_memoryview_fromslice(__pyx_v_D, 2, (PyObject *(*)(char *)) __pyx_memview_get_double, (int (*)(char *, PyObject *)) __pyx_memview_set_double, 0);; if (unlikely(!__pyx_t_4)) __PYX_ERR(0, 21, __pyx_L1_error)
  __Pyx_GOTREF(__pyx_t_4);
  __pyx_t_2 = NULL;
  if (CYTHON_UNPACK_METHODS && unlikely(PyMethod_Check(__pyx_t_3))) {
    __pyx_t_2 = PyMethod_GET_SELF(__pyx_t_3);
    if (likely(__pyx_t_2)) {
      PyObject* function = PyMethod_GET_FUNCTION(__pyx_t_3);
      __Pyx_INCREF(__pyx_t_2);
      __Pyx_INCREF(function);
      __Pyx_DECREF_SET(__pyx_t_3, function);
    }
  }
  if (!__pyx_t_2) {
    __pyx_t_5 = __Pyx_PyObject_CallOneArg(__pyx_t_3, __pyx_t_4); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 21, __pyx_L1_error)
    __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
    __Pyx_GOTREF(__pyx_t_5);
  } else {
    #if CYTHON_FAST_PYCALL
    if (PyFunction_Check(__pyx_t_3)) {
      PyObject *__pyx_temp[2] = {__pyx_t_2, __pyx_t_4};
      __pyx_t_5 = __Pyx_PyFunction_FastCall(__pyx_t_3, __pyx_temp+1-1, 1+1); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 21, __pyx_L1_error)
      __Pyx_XDECREF(__pyx_t_2); __pyx_t_2 = 0;
      __Pyx_GOTREF(__pyx_t_5);
      __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
    } else
    #endif
    #if CYTHON_FAST_PYCCALL
    if (__Pyx_PyFastCFunction_Check(__pyx_t_3)) {
      PyObject *__pyx_temp[2] = {__pyx_t_2, __pyx_t_4};
      __pyx_t_5 = __Pyx_PyCFunction_FastCall(__pyx_t_3, __pyx_temp+1-1, 1+1); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 21, __pyx_L1_error)
      __Pyx_XDECREF(__pyx_t_2); __pyx_t_2 = 0;
      __Pyx_GOTREF(__pyx_t_5);
      __Pyx_DECREF(__pyx_t_4); __pyx_t_4 = 0;
    } else
    #endif
    {
      __pyx_t_1 = PyTuple_New(1+1); if (unlikely(!__pyx_t_1)) __PYX_ERR(0, 21, __pyx_L1_error)
      __Pyx_GOTREF(__pyx_t_1);
      __Pyx_GIVEREF(__pyx_t_2); PyTuple_SET_ITEM(__pyx_t_1, 0, __pyx_t_2); __pyx_t_2 = NULL;
      __Pyx_GIVEREF(__pyx_t_4);
      PyTuple_SET_ITEM(__pyx_t_1, 0+1, __pyx_t_4);
      __pyx_t_4 = 0;
      __pyx_t_5 = __Pyx_PyObject_Call(__pyx_t_3, __pyx_t_1, NULL); if (unlikely(!__pyx_t_5)) __PYX_ERR(0, 21, __pyx_L1_error)
      __Pyx_GOTREF(__pyx_t_5);
      __Pyx_DECREF(__pyx_t_1); __pyx_t_1 = 0;
    }
  }
  __Pyx_DECREF(__pyx_t_3); __pyx_t_3 = 0;
  __pyx_r = __pyx_t_5;
  __pyx_t_5 = 0;
  goto __pyx_L0;
In [29]:
assert np.allclose(pairwise_numpy(X), pairwise_cython(X), rtol=1e-10, atol=1e-15)
In [30]:
timings = %timeit -o pairwise_cython(X)
pairwise_times['cython2'] = timings
4.64 ms ± 82.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Tohle je už konečně výrazné zrychlení oproti NumPy.

Cython toho nabízí mnoho

Podívejte se na http://docs.cython.org co všechno Cython nabízí -- není toho málo, např.

  • použití C++
  • šablony (templates)
  • OpenMP (k tomu se možná ještě dostaneme)
  • vytváření C-API
  • třídy (cdef classes)

Z Fortranu (nebo C) do Pythonu pomocí F2PY

F2PY je nástroj, který byl v podstatě vytvořen pro NumPy a SciPy, protože, jak dobře víme, tyto moduly volají externí knihovny napsané ve Fortrane nebo C. Dokumentaci (trochu zastaralou) najdeme zde. Bylo tedy velice výhodné vytvořit nástroj, který toto usnadní. A tak se zrodilo F2PY. Ve zkratce, F2PY umožňuje velice jednoduše z Fortran nebo C funkcí vytvořit Python modul. Využívá navíc vlastností NumPy pro předávání vícerozměnrých polí.

Poďme chvilku programovat ve Fortranu :)

In [31]:
%%file pairwise_fort.f90

subroutine pairwise_fort(X, D, m, n)
    integer :: n,m
    double precision, intent(in) :: X(m, n)
    double precision, intent(out) :: D(m, m) 
    integer :: i, j, k
    double precision :: r 
    
    do j = 1,m 
        do i = 1,m 
            r = 0
            do k = 1,n 
                r = r + (X(i,k) - X(j,k)) * (X(i,k) - X(j,k)) 
            end do 
            D(i,j) = sqrt(r) 
        end do 
    end do 
end subroutine pairwise_fort
Overwriting pairwise_fort.f90

Z čeho f2py bere informace o vytvoření modulu?

  1. Pole (double precision) převádí na numpy array.
  2. intent(in) = vstupní argument.
  3. intent(out) = výstupní argument.
  4. f2py schová explicitně zadané rozměry polí (m, n).

Pokud bychom programovali v C, je potřeba dodat f2py nějaké informace navíc, neboť např. intent v C neexistuje.

Tento soubor přeložíme pomocí f2py:

In [32]:
!f2py -c pairwise_fort.f90 -m pairwise_fort 
running build
running config_cc
unifing config_cc, config, build_clib, build_ext, build commands --compiler options
running config_fc
unifing config_fc, config, build_clib, build_ext, build commands --fcompiler options
running build_src
build_src
building extension "pairwise_fort" sources
f2py options: []
f2py:> /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/pairwise_fortmodule.c
creating /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6
Reading fortran codes...
	Reading file 'pairwise_fort.f90' (format:free)
Post-processing...
	Block: pairwise_fort
			Block: pairwise_fort
Post-processing (stage 2)...
Building modules...
	Building module "pairwise_fort"...
		Constructing wrapper function "pairwise_fort"...
		  d = pairwise_fort(x,[m,n])
	Wrote C/API module "pairwise_fort" to file "/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/pairwise_fortmodule.c"
  adding '/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.c' to sources.
  adding '/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6' to include_dirs.
copying /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/f2py/src/fortranobject.c -> /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6
copying /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/f2py/src/fortranobject.h -> /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6
build_src: building npy-pkg config files
running build_ext
customize UnixCCompiler
customize UnixCCompiler using build_ext
get_default_fcompiler: matching types: '['gnu95', 'intel', 'lahey', 'pg', 'absoft', 'nag', 'vast', 'compaq', 'intele', 'intelem', 'gnu', 'g95', 'pathf95', 'nagfor']'
customize Gnu95FCompiler
Found executable /usr/bin/gfortran
customize Gnu95FCompiler
customize Gnu95FCompiler using build_ext
building 'pairwise_fort' extension
compiling C sources
C compiler: gcc -pthread -B /home/kuba/opt/conda/envs/fjfi/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC

creating /tmp/tmpsgb5rs8i/tmp
creating /tmp/tmpsgb5rs8i/tmp/tmpsgb5rs8i
creating /tmp/tmpsgb5rs8i/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6
compile options: '-I/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6 -I/home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include -I/home/kuba/opt/conda/envs/fjfi/include/python3.6m -c'
gcc: /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/pairwise_fortmodule.c
In file included from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/ndarraytypes.h:1818:0,
                 from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/ndarrayobject.h:18,
                 from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/arrayobject.h:4,
                 from /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.h:13,
                 from /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/pairwise_fortmodule.c:16:
/home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:15:2: warning: #warning "Using deprecated NumPy API, disable it by " "#defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
 #warning "Using deprecated NumPy API, disable it by " \
  ^~~~~~~
/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/pairwise_fortmodule.c:109:12: warning: ‘f2py_size’ defined but not used [-Wunused-function]
 static int f2py_size(PyArrayObject* var, ...)
            ^~~~~~~~~
gcc: /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.c
In file included from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/ndarraytypes.h:1818:0,
                 from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/ndarrayobject.h:18,
                 from /home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/arrayobject.h:4,
                 from /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.h:13,
                 from /tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.c:2:
/home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include/numpy/npy_1_7_deprecated_api.h:15:2: warning: #warning "Using deprecated NumPy API, disable it by " "#defining NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION" [-Wcpp]
 #warning "Using deprecated NumPy API, disable it by " \
  ^~~~~~~
/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.c: In function ‘format_def’:
/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.c:138:18: warning: comparison between signed and unsigned integer expressions [-Wsign-compare]
         if (size < sizeof(notalloc)) {
                  ^
compiling Fortran sources
Fortran f77 compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran f90 compiler: /usr/bin/gfortran -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
Fortran fix compiler: /usr/bin/gfortran -Wall -g -ffixed-form -fno-second-underscore -Wall -g -fno-second-underscore -fPIC -O3 -funroll-loops
compile options: '-I/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6 -I/home/kuba/opt/conda/envs/fjfi/lib/python3.6/site-packages/numpy/core/include -I/home/kuba/opt/conda/envs/fjfi/include/python3.6m -c'
gfortran:f90: pairwise_fort.f90
/usr/bin/gfortran -Wall -g -Wall -g -shared /tmp/tmpsgb5rs8i/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/pairwise_fortmodule.o /tmp/tmpsgb5rs8i/tmp/tmpsgb5rs8i/src.linux-x86_64-3.6/fortranobject.o /tmp/tmpsgb5rs8i/pairwise_fort.o -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7 -lgfortran -o ./pairwise_fort.cpython-36m-x86_64-linux-gnu.so
Removing build directory /tmp/tmpsgb5rs8i

-m pairwise_fort je požadované jméno modulu. Můžeme ho rovnou importovat, resp. jeho stejnojmennou funkci.

In [33]:
from pairwise_fort import pairwise_fort
print(pairwise_fort.__doc__)
d = pairwise_fort(x,[m,n])

Wrapper for ``pairwise_fort``.

Parameters
----------
x : input rank-2 array('d') with bounds (m,n)

Other Parameters
----------------
m : input int, optional
    Default: shape(x,0)
n : input int, optional
    Default: shape(x,1)

Returns
-------
d : rank-2 array('d') with bounds (m,m)

Fortran a C používají jiné uspořádání paměti pro ukládání vícerozměrných polí. Fortran je "column-major" zatímco C je "row-major". NumPy dokáže pracovat s obojím a pro uživatele je to obvykle jedno. Pokud ovšem chceme předat vícerozměrné pole do Fortran funkce, je lepší mít prvky uložené v paměti jako to dělá Fortran. V takovém případě totiž f2py předá pouze referenci (ukazatel) na dané místo v paměti. V opačném případě f2py nejprve pole musí transponovat, tj. vytvořit kopii s jiným uspořádáním, což může být samozřejmě náročné na pamět a procesor.

Vytvoříme si proměnnou XF, která má Fortran uspořádání, pomocí numpy.asfortranarray (prozaický název :)

In [34]:
XF = np.asfortranarray(X)

Vyzkoušíme, jestli stále dostáváme stejné výsledky.

In [35]:
assert np.allclose(pairwise_numpy(X), pairwise_fort(X), rtol=1e-10, atol=1e-15)

No a konečně se můžeme podívat, jak je to s rychlostí ...

In [36]:
timings = %timeit -o pairwise_fort(X)
pairwise_times['fortran'] = timings
7.1 ms ± 169 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Představuje se numba

Numba kompiluje Python kód pomocí LLVM. Podporujme just-in-time kompilaci pomocí dekorátoru jit (http://numba.pydata.org/numba-doc/latest/user/jit.html).

@numba.jit(
    signature=None, 
    nopython=False, 
    nogil=False, 
    cache=False, 
    forceobj=False, 
    parallel=False, 
    error_model='python', 
    fastmath=False, locals={}
)
In [37]:
import numba
In [38]:
pairwise_numba = numba.jit(pairwise_python)

Tradiční kontrola. Po prvním spuštění navíc Numba funkci poprvé zkompiluje.

In [39]:
assert np.allclose(pairwise_numpy(X), pairwise_numba(X), rtol=1e-10, atol=1e-15)

Jaký čas od takto "jednoduché" optimalizace můžeme očekávat?

In [40]:
timings = %timeit -o pairwise_numba(X)
pairwise_times['numba'] = timings
5.01 ms ± 261 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Vidíme, že zrychlení je výborné - jsme na úrovni zatím nejlepšího výsledku! A navíc že jsme toho dosáhli jediným řádkem (kromě importů).

Ještě můžeme zkusit výledek vylepšit pomocí paralelizace, nopython a / nebo fastmath režimu. Pro nopython musíme vytvořit výsledný numpy objekt vně kompilované funkce. Paralelizace docílíme pomocí parallel=True a numba.prange. Všimněte si použití @jit jako dekorátoru.

In [41]:
@numba.jit(nopython=True, parallel=True, fastmath=True)
def _pairwise_nopython(X: numpy.ndarray, D: numpy.ndarray) -> numpy.ndarray:
    M = X.shape[0]
    N = X.shape[1]
    for i in numba.prange(M):
        for j in numba.prange(M):
            d = 0.0
            for k in range(N):
                tmp = X[i, k] - X[j, k]
                d += tmp * tmp
            D[i, j] = numpy.sqrt(d)
    return D


def pairwise_numba_fast_parallel(X: numpy.ndarray) -> numpy.ndarray:
    D = numpy.empty((X.shape[0], X.shape[0]), dtype = numpy.float)
    _pairwise_nopython(X, D)
    return D
In [42]:
assert np.allclose(pairwise_numpy(X), pairwise_numba_fast_parallel(X), rtol=1e-10, atol=1e-15)
In [43]:
timings = %timeit -o pairwise_numba_fast_parallel(X)
pairwise_times['numba_fast_parallel'] = timings
3.25 ms ± 529 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Srovnání výsledků

Výsledky můžeme porovnat pomocí grafu.

In [44]:
pairwise_times
Out[44]:
OrderedDict([('plain_python',
              <TimeitResult : 4.78 s ± 66.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)>),
             ('numpy',
              <TimeitResult : 38.8 ms ± 221 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)>),
             ('cython0',
              <TimeitResult : 4.6 s ± 73.3 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)>),
             ('cython1',
              <TimeitResult : 165 ms ± 2.8 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)>),
             ('cython2',
              <TimeitResult : 4.64 ms ± 82.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)>),
             ('fortran',
              <TimeitResult : 7.1 ms ± 169 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)>),
             ('numba',
              <TimeitResult : 5.01 ms ± 261 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)>),
             ('numba_fast_parallel',
              <TimeitResult : 3.25 ms ± 529 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)>)])
In [45]:
fig, ax = plt.subplots()
values = np.array([t.average for t in pairwise_times.values()])
x = range(len(pairwise_times))
ax.bar(x, values)
ax.set_xticks(x)
ax.set_xticklabels(tuple(pairwise_times.keys()), rotation='vertical')
ax.set_ylabel('time [ms]')
ax.set_yscale('log')

Další možnosti

  • Vestavěný modul ctypes dovoluje volat funkce z externích dynamických knihoven. Pro použití s NumPy viz Cookbook.
  • Alternativou k ctypes je cffi.
  • CuPy využívá GPU.
  • numexpr dokáže kompilovat Numpy výrazy.
  • Theano se zaměřuje na strojové učení, také optimalizuje vektorové (maticové) operace, dovoluje je spouštět na CPU.
  • Nuitka kompiluje Python, ale na rozdíl od Numba nespecializuje funkce na základě typů.
  • SWIG jde použít pro propojení s mnoha jazyky.'

Cvičení

Obdobný postup aplikujte na výpočet kumulativního součtu, který je definovaný jako

$\displaystyle S_j = \sum\limits_{i = 1}^j x_i $

Výsledky a časování porovnejte s numpy.cumsum.

Nápověda: Ve vaší funkci vytvořte nejprve výsledné numpy pole pomocí numpy.empty_like.

Komentáře

Comments powered by Disqus