diff --git a/.travis.yml b/.travis.yml index 3839a14..409ddf5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,11 +11,7 @@ install: - cd src - python setup.py build_ext --inplace script: - - python -m timeit -s "import Fibo" "Fibo.fib(30)" - - python -m timeit -s "import cyFibo" "cyFibo.fib(30)" - - python -m timeit -s "import cyFibo" "cyFibo.fib_int(30)" - - python -m timeit -s "import cyFibo" "cyFibo.fib_cdef(30)" - - python -m timeit -s "import cyFibo" "cyFibo.fib_cpdef(30)" - - python -m timeit -s "import Fibo" "Fibo.fib_cached(30)" + - python -m unittest discover + - python fibo_bench.py - python -m timeit -s "import cyStdDev; import numpy as np; a = np.arange(1e6)" "cyStdDev.cStdDev(a)" - python FiboTimeit.py diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..68a758d --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\pikos.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\pikos.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/doc/source/fibo_speed.rst b/doc/source/fibo_speed.rst index b16205b..a57247b 100644 --- a/doc/source/fibo_speed.rst +++ b/doc/source/fibo_speed.rst @@ -44,125 +44,49 @@ In Cython calling C generated code. Here we use a ``def`` to call a ``cdef`` tha return n return fib_in_c(n-2) + fib_in_c(n-1) -Now a recursive ``cpdef``:: +Now a recursive ``cpdef`` returning a python object:: cpdef fib_cpdef(int n): if n < 2: return n return fib_cpdef(n-2) + fib_cpdef(n-1) +A recursive ``cpdef`` returning an int:: + + cpdef int fib_typed_cpdef(int n): + if n < 2: + return n + return fib_typed_cpdef(n-2) + fib_typed_cpdef(n-1) + Finally a C extension. We expect this to be the fastest way of computing the result given the algorithm we have chosen: -.. code-block:: c - - #include "Python.h" - - /* This is the function that actually computes the Fibonacci value. */ - static long c_fibonacci(long ord) { - if (ord < 2) { - return ord; - } - return c_fibonacci(ord - 2) + c_fibonacci(ord -1); - } - - /* The Python interface to the C code. */ - static PyObject *python_fibonacci(PyObject *module, PyObject *arg) { - PyObject *ret = NULL; - assert(arg); - Py_INCREF(arg); - if (! PyLong_CheckExact(arg)) { - PyErr_SetString(PyExc_ValueError, "Argument is not an integer."); - goto except; - } - long ordinal = PyLong_AsLong(arg); - long result = c_fibonacci(ordinal); - ret = PyLong_FromLong(result); - assert(! PyErr_Occurred()); - assert(ret); - goto finally; - except: - Py_XDECREF(ret); - ret = NULL; - finally: - Py_DECREF(arg); - return ret; - } - - /********* The rest is standard Python Extension code ***********/ - - - static PyMethodDef cFiboExt_methods[] = { - {"fib", python_fibonacci, METH_O, "Fibonacci value."}, - {NULL, NULL, 0, NULL} /* sentinel */ - }; - - - #if PY_MAJOR_VERSION >= 3 - - /********* PYTHON 3 Boilerplate ***********/ - - PyDoc_STRVAR(module_doc, "Fibonacci in C."); - - static struct PyModuleDef cFiboExt = { - PyModuleDef_HEAD_INIT, - "cFibo", - module_doc, - -1, - cFiboExt_methods, - NULL, - NULL, - NULL, - NULL - }; - - PyMODINIT_FUNC - PyInit_cFibo(void) - { - return PyModule_Create(&cFiboExt); - } - - #else - - /********* PYTHON 2 Boilerplate ***********/ - - - PyMODINIT_FUNC - initcFibo(void) - { - (void) Py_InitModule("cFibo", cFiboExt_methods); - } - - #endif +.. literalinclude:: ../../src/cFiboExt.c + :language: c Benchmarks ------------------- -First a correctness check on Fibonacci(30):: +First a correctness check on the methods:: - $ python3 -c "import Fibo, cyFibo, cFibo; print(Fibo.fib(30) == cyFibo.fib(30) == cyFibo.fib_int(30) == cyFibo.fib_cdef(30) == cyFibo.fib_cpdef(30) == cFibo.fib(30))" - True + python -m unittest discover Now time these algorithms on Fibonacci(30) thus:: - $ python3 -m timeit -s "import Fibo" "Fibo.fib(30)" - $ python3 -m timeit -s "import cyFibo" "cyFibo.fib(30)" - $ python3 -m timeit -s "import cyFibo" "cyFibo.fib_int(30)" - $ python3 -m timeit -s "import cyFibo" "cyFibo.fib_cdef(30)" - $ python3 -m timeit -s "import cyFibo" "cyFibo.fib_cpdef(30)" - $ python3 -m timeit -s "import cFibo" "cFibo.fib(30)" + python fibo_bench.py Gives: -======== =========================== ============= ================= -Language Function call Time (ms) Speed, Python = 1 -======== =========================== ============= ================= -Python ``Fibo.fib(30)`` 390 x 1 -Cython ``cyFibo.fib(30)`` 215 x 1.8 -Cython ``cyFibo.fib_int(30)`` 154 x 2.5 -Cython ``cyFibo.fib_cdef(30)`` 5.38 x72 -Cython ``cyFibo.fib_cpdef(30)`` 32.5 x12 -C ``cFibo.fib(30)`` 5.31 x73 -======== =========================== ============= ================= +======== ============================ ============= ================= +Language Function call Time (ms) Speed, Python = 1 +======== ============================ ============= ================= +Python ``Fibo.fib(30)`` 571 x1 +Cython ``cyFibo.fib(30)`` 229 x2.5 +Cython ``cyFibo.fib_int(30)`` 165 x3.5 +Cython ``cyFibo.fib_cdef(30)`` 7.31 x78 +Cython ``cyFibo.fib_cpdef(30)`` 39.6 x14 +Cython ``cyFibo.fib_int cpdef(30)`` 5.61 x102 +C ``cFibo.fib(30)`` 6.75 x85 +======== ============================ ============= ================= Graphically: @@ -170,16 +94,25 @@ Graphically: The conclusions that I draw from this are: -* Naive Cython does speed things up, but not by much (x1.8). -* Optimised Cython is fairly effortless (in this case) and worthwhile (x2.5). -* ``cdef`` is really valuable (x72). -* ``cpdef`` gives a good improvement over ``def`` because the recursive case exploits C functions. -* Cython's ``cdef`` is insignificantly different from the more complicated C extension that is our best attempt. +* Naive Cython does speed things up, but not by much (x2.5). +* Optimised Cython is fairly effortless (in this case) and worthwhile + (x3.5). +* ``cpdef`` gives a good improvement over ``def`` because the + recursive case exploits C functions. +* ``cdef`` is really valuable (x78). +* Cython's ``cdef`` is insignificantly different from the more + complicated C extension that is our best attempt. +* ``typed cpdef`` gives the best of two worlds and (in our example) it + is even faster than the hand wrapping of the C function. The Importance of the Algorithm ------------------------------------- -So far we have looked at pushing code into Cython/C to get a performance gain however there is a glaring error in our code. The algorithm we have been using is **very** inefficient. Here is different algorithm, in pure Python, that will beat all of those above by a huge margin [#]_: +So far we have looked at pushing code into Cython/C to get a +performance gain however there is a glaring error in our code. The +algorithm we have been using is **very** inefficient. Here is +different algorithm, in pure Python, that will beat all of those above +by a huge margin [#]_: .. code-block:: python @@ -207,12 +140,16 @@ Or, graphically: .. image:: images/CacheComparison.png -In fact our new algorithm is far, far better than that. Here is the O(N) behaviour where N is the Fibonacci ordinal: +In fact our new algorithm is far, far better than that. Here is the +O(N) behaviour where N is the Fibonacci ordinal: .. image:: images/CacheON.png -Hammering a bad algorithm with a fast language is worse than using a good algorithm and a slow language. +Hammering a bad algorithm with a fast language is worse than using a +good algorithm and a slow language. .. rubric:: Footnotes -.. [#] If you are using Python3 you can use the ``functools.lru_cache`` decorator that gives you more control over cache behaviour. +.. [#] If you are using Python3 you can use the + ``functools.lru_cache`` decorator that gives you more control + over cache behaviour. diff --git a/doc/source/images/FibonacciComparison.png b/doc/source/images/FibonacciComparison.png index b39b489..3221e21 100644 Binary files a/doc/source/images/FibonacciComparison.png and b/doc/source/images/FibonacciComparison.png differ diff --git a/src/cFiboExt.c b/src/cFiboExt.c index afce1b7..2bb44f7 100644 --- a/src/cFiboExt.c +++ b/src/cFiboExt.c @@ -13,14 +13,18 @@ static PyObject *python_fibonacci(PyObject *module, PyObject *arg) { PyObject *ret = NULL; assert(arg); Py_INCREF(arg); - if (! PyLong_CheckExact(arg)) { +#if PY_MAJOR_VERSION >= 3 + if (!PyLong_Check(arg)) { +#else + if (!PyLong_Check(arg) & !PyInt_Check(arg)) { +#endif PyErr_SetString(PyExc_ValueError, "Argument is not an integer."); goto except; } long ordinal = PyLong_AsLong(arg); long result = c_fibonacci(ordinal); ret = PyLong_FromLong(result); - assert(! PyErr_Occurred()); + assert(!PyErr_Occurred()); assert(ret); goto finally; except: diff --git a/src/cyFibo.pyx b/src/cyFibo.pyx index 1ca14c7..8681b53 100755 --- a/src/cyFibo.pyx +++ b/src/cyFibo.pyx @@ -11,25 +11,33 @@ def fib(n): return fib(n-2) + fib(n-1) -def fib_int(int n): +def fib_int(long n): """Vanilla Python with type specification.""" if n < 2: return n return fib_int(n-2) + fib_int(n-1) -def fib_cdef(int n): +def fib_cdef(long n): """Call a cdef.""" return fib_in_c(n) -cdef int fib_in_c(int n): +cdef long fib_in_c(long n): if n < 2: return n return fib_in_c(n-2) + fib_in_c(n-1) -cpdef fib_cpdef(int n): + +cpdef fib_cpdef(long n): """Basic cpdef.""" if n < 2: return n return fib_cpdef(n-2) + fib_cpdef(n-1) + + +cpdef long fib_int_cpdef(long n): + """Typed cpdef.""" + if n < 2: + return n + return fib_int_cpdef(n-2) + fib_int_cpdef(n-1) diff --git a/src/fibo_bench.py b/src/fibo_bench.py new file mode 100644 index 0000000..6f7cd05 --- /dev/null +++ b/src/fibo_bench.py @@ -0,0 +1,28 @@ +from timeit import Timer +from timeit import main as console_timeit +from collections import namedtuple, OrderedDict + +Bench = namedtuple('Bench', ['setup', 'call']) + + +methods = OrderedDict([ + ('Python', Bench("import Fibo", "Fibo.fib({})")), + ('Cython naive', Bench("import cyFibo", "cyFibo.fib({})")), + ('Cython typed', Bench("import cyFibo", "cyFibo.fib_int({})")), + ('Cython cdef', Bench("import cyFibo", "cyFibo.fib_cdef({})")), + ('Cython cpdef', Bench("import cyFibo", "cyFibo.fib_cpdef({})")), + ('Cython typed cpdef', + Bench("import cyFibo", "cyFibo.fib_int_cpdef({})")), + ('Wrapped C', Bench("import cFibo", "cFibo.fib({})")), + ('Python alt', Bench("import Fibo", "Fibo.fib_cached({})"))]) + + +def main(): + for method in methods: + print(method) + console_timeit([ + methods[method].setup, + methods[method].call.format(30)]) + +if __name__ == '__main__': + main() diff --git a/src/makeplot.py b/src/makeplot.py new file mode 100644 index 0000000..4132ec6 --- /dev/null +++ b/src/makeplot.py @@ -0,0 +1,16 @@ +from matplotlib import pylab +from matplotlib.ticker import ScalarFormatter + +import numpy +import seaborn +seaborn.set(style="white", context="talk") +time = numpy.array([571, 229, 165, 7.31, 39.6, 5.61, 6.75]) +labels= numpy.array([ + 'Python', 'def() naive', 'def() typed', 'cdef()', 'cpdef()', 'cpdef typed', 'C']) +axes = seaborn.barplot(y=time, x=labels, x_order=labels) +axes.yaxis.label.set_text("Time (ms)") +axes.yaxis.grid(color='black', which='both') +axes.set_yscale('log') +axes.set_ylim(1, 1000) +axes.yaxis.set_major_formatter(ScalarFormatter()) +pylab.show() diff --git a/src/test_fibo.py b/src/test_fibo.py new file mode 100644 index 0000000..4bde176 --- /dev/null +++ b/src/test_fibo.py @@ -0,0 +1,39 @@ +import unittest + +from Fibo import fib, fib_cached +from cFibo import fib as cfib +from cyFibo import fib as cyfib +from cyFibo import fib_cdef, fib_int, fib_cpdef, fib_int_cpdef + + +class TestFibo(unittest.TestCase): + + def _check_fibonacci(self, function): + expected = [ + 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144] + result = [function(i) for i in range(13)] + self.assertEqual(expected, result) + + def test_fib(self): + self._check_fibonacci(fib) + + def test_cfib(self): + self._check_fibonacci(cfib) + + def test_cyfib(self): + self._check_fibonacci(cyfib) + + def test_fib_cdef(self): + self._check_fibonacci(fib_cdef) + + def test_fib_cpdef(self): + self._check_fibonacci(fib_cpdef) + + def test_fib_int_cpdef(self): + self._check_fibonacci(fib_int_cpdef) + + def test_fib_int(self): + self._check_fibonacci(fib_int) + + def test_fib_cached(self): + self._check_fibonacci(fib_cached)