Skip to content

Instantly share code, notes, and snippets.

@vchahun
Created June 15, 2013 18:31
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save vchahun/5789076 to your computer and use it in GitHub Desktop.
Save vchahun/5789076 to your computer and use it in GitHub Desktop.
python array benchmarks
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"name": "NumbaCython"
},
"nbformat": 3,
"nbformat_minor": 0,
"worksheets": [
{
"cells": [
{
"cell_type": "heading",
"level": 1,
"metadata": {},
"source": [
"Numba vs. Cython: Take 2.2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"*This notebook first appeared as a*\n",
"[*post*](http://jakevdp.github.io/blog/2012/08/24/numba-vs-cython/)\n",
"*by Jake Vanderplas on the blog*\n",
"[*Pythonic Perambulations*](http://jakevdp.github.io)"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"import numpy as np\n",
"X = np.random.random((1000, 3))"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 1
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Numpy Function With Broadcasting"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def pairwise_numpy(X):\n",
" return np.sqrt(((X[:, None, :] - X) ** 2).sum(-1))\n",
"%timeit -n 10 pairwise_numpy(X)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"10 loops, best of 3: 65.1 ms per loop\n"
]
}
],
"prompt_number": 2
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Pure Python Function"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"def pairwise_python(X):\n",
" M = X.shape[0]\n",
" N = X.shape[1]\n",
" D = np.empty((M, M), dtype=np.float)\n",
" for i in xrange(M):\n",
" for j in xrange(M):\n",
" d = 0.0\n",
" for k in xrange(N):\n",
" tmp = X[i, k] - X[j, k]\n",
" d += tmp * tmp\n",
" D[i, j] = np.sqrt(d)\n",
" return D\n",
"%timeit -n 1 pairwise_python(X)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"1 loops, best of 3: 8.24 s per loop\n"
]
}
],
"prompt_number": 3
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"PyPy"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%%pypy\n",
"import timeit\n",
"\n",
"setup = \"\"\"\n",
"import numpypy as np\n",
"import random\n",
"\n",
"X = np.zeros((1000, 3))\n",
"for i in xrange(X.shape[0]):\n",
" for j in xrange(X.shape[1]):\n",
" X[i, j] = random.random()\n",
"\n",
"def pairwise_pypy(X):\n",
" M = X.shape[0]\n",
" N = X.shape[1]\n",
" D = np.empty((M, M), dtype=float)\n",
" for i in xrange(M):\n",
" for j in xrange(M):\n",
" d = 0.0\n",
" for k in xrange(N):\n",
" tmp = X[i, k] - X[j, k]\n",
" d += tmp * tmp\n",
" D[i, j] = np.sqrt(d)\n",
" return D\n",
"\"\"\"\n",
"print timeit.timeit('pairwise_pypy(X)', setup, number=100)/100."
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"0.071649107933\n"
]
}
],
"prompt_number": 4
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Numba Wrapper"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"from numba import double\n",
"from numba.decorators import jit, autojit\n",
"\n",
"pairwise_numba = autojit(pairwise_python)\n",
"\n",
"%timeit -n 100 pairwise_numba(X)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"100 loops, best of 3: 8 ms per loop\n"
]
}
],
"prompt_number": 5
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Optimized Cython Function"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%load_ext cythonmagic"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 6
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%%cython\n",
"\n",
"import numpy as np\n",
"cimport cython\n",
"from libc.math cimport sqrt\n",
"\n",
"@cython.boundscheck(False)\n",
"@cython.wraparound(False)\n",
"def pairwise_cython(double[:, ::1] X):\n",
" cdef unsigned M = X.shape[0]\n",
" cdef unsigned N = X.shape[1]\n",
" cdef double tmp, d\n",
" cdef double[:, ::1] D = np.empty((M, M), dtype=np.float64)\n",
" cdef unsigned i, j, k\n",
" for i in range(M):\n",
" for j in range(M):\n",
" d = 0.0\n",
" for k in range(N):\n",
" tmp = X[i, k] - X[j, k]\n",
" d += tmp * tmp\n",
" D[i, j] = sqrt(d)\n",
" return np.asarray(D)"
],
"language": "python",
"metadata": {},
"outputs": [],
"prompt_number": 7
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%timeit -n 100 pairwise_cython(X)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"100 loops, best of 3: 7.97 ms per loop\n"
]
}
],
"prompt_number": 8
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Scipy Pairwise Distances"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"from scipy.spatial.distance import cdist\n",
"%timeit cdist(X, X)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"100 loops, best of 3: 9 ms per loop\n"
]
}
],
"prompt_number": 9
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Scikit-learn Pairwise Distances"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"from sklearn.metrics import euclidean_distances\n",
"%timeit euclidean_distances(X, X)"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"100 loops, best of 3: 17.4 ms per loop\n"
]
}
],
"prompt_number": 10
},
{
"cell_type": "heading",
"level": 2,
"metadata": {},
"source": [
"Comparing the Results"
]
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"%pylab inline"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "stream",
"stream": "stdout",
"text": [
"\n",
"Welcome to pylab, a matplotlib-based Python environment [backend: module://IPython.zmq.pylab.backend_inline].\n",
"For more information, type 'help(pylab)'.\n"
]
}
],
"prompt_number": 11
},
{
"cell_type": "code",
"collapsed": false,
"input": [
"labels = ['python\\nloop', 'pypy', 'numpy\\nbroadc.', 'sklearn', 'scipy', 'cython', 'numba']\n",
"timings = [8.24, 0.0716, 0.0651, 0.0174, 0.009, 0.00797, 0.00800]\n",
"x = np.arange(len(labels))\n",
"\n",
"ax = plt.axes(xticks=x, yscale='log')\n",
"ax.bar(x - 0.3, timings, width=0.6, alpha=0.4, bottom=1E-6)\n",
"ax.grid()\n",
"ax.set_xlim(-0.5, len(labels) - 0.5)\n",
"ax.set_ylim(1E-3, 1E2)\n",
"ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda i, loc: labels[int(i)]))\n",
"ax.set_ylabel('time (s)')\n",
"ax.set_title(\"Pairwise Distance Timings\");"
],
"language": "python",
"metadata": {},
"outputs": [
{
"output_type": "display_data",
"png": "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\n"
}
],
"prompt_number": 12
},
{
"cell_type": "code",
"collapsed": false,
"input": [],
"language": "python",
"metadata": {},
"outputs": []
}
],
"metadata": {}
}
]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment