Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cython: are typed memoryviews the modern way to type numpy arrays?

Tags:

Let's say I'd like to pass a numpy array to a cdef function:

cdef double mysum(double[:] arr):     cdef int n = len(arr)     cdef double result = 0      for i in range(n):         result = result + arr[i]      return result 

Is this the modern way to handle typing numpy arrays? Compare with this question: cython / numpy type of an array

What if I want to do the following:

cdef double[:] mydifference(int a, int b):     cdef double[:] arr_a = np.arange(a)     cdef double[:] arr_b = np.arange(b)      return arr_a - arr_b 

This will return an error because - is not defined for memoryviews. So, should that case have been handled as follows?

cdef double[:] mydifference(int a, int b):     arr_a = np.arange(a)     arr_b = np.arange(b)      return arr_a - arr_b 
like image 928
bzm3r Avatar asked Oct 04 '14 04:10

bzm3r


People also ask

Does NumPy use Cython?

You can use NumPy from Cython exactly the same as in regular Python, but by doing so you are losing potentially high speedups because Cython has support for fast access to NumPy arrays. Let's see how this works with a simple example.

Does Cython improve NumPy?

By explicitly declaring the "ndarray" data type, your array processing can be 1250x faster. This tutorial will show you how to speed up the processing of NumPy arrays using Cython. By explicitly specifying the data types of variables in Python, Cython can give drastic speed increases at runtime.


1 Answers

I will quote from the docs the docs

Memoryviews are similar to the current NumPy array buffer support (np.ndarray[np.float64_t, ndim=2]), but they have more features and cleaner syntax.

This indicates that the developers of Cython consider memory views to be the modern way.

Memory views offer some big advantages over the np.ndarray notation primarily in elegance and interoperability, however they are not superior in performance.

Performance:

First it should be noted that boundscheck sometimes fails to work with memory views resulting in artificially fast figures for memoryviews with boundscheck=True (i.e. you get fast, unsafe indexing), if you're relying on boundscheck to catch bugs this could be a nasty surprise.

For the most part once compiler optimizations have been applied, memory views and numpy array notation are equal in performance, often precisely so. When there is a difference it is normally no more than 10-30%.

Performance benchmark

The number is the time in seconds to perform 100,000,000 operations. Smaller is faster.

ACCESS+ASSIGNMENT on small array (10000 elements, 10000 times) Results for `uint8` 1) memory view: 0.0415 +/- 0.0017 2) np.ndarray : 0.0531 +/- 0.0012 3) pointer    : 0.0333 +/- 0.0017  Results for `uint16` 1) memory view: 0.0479 +/- 0.0032 2) np.ndarray : 0.0480 +/- 0.0034 3) pointer    : 0.0329 +/- 0.0008  Results for `uint32` 1) memory view: 0.0499 +/- 0.0021 2) np.ndarray : 0.0413 +/- 0.0005 3) pointer    : 0.0332 +/- 0.0010  Results for `uint64` 1) memory view: 0.0489 +/- 0.0019 2) np.ndarray : 0.0417 +/- 0.0010 3) pointer    : 0.0353 +/- 0.0017  Results for `float32` 1) memory view: 0.0398 +/- 0.0027 2) np.ndarray : 0.0418 +/- 0.0019 3) pointer    : 0.0330 +/- 0.0006  Results for `float64` 1) memory view: 0.0439 +/- 0.0037 2) np.ndarray : 0.0422 +/- 0.0013 3) pointer    : 0.0353 +/- 0.0013  ACCESS PERFORMANCE (100,000,000 element array): Results for `uint8` 1) memory view: 0.0576 +/- 0.0006 2) np.ndarray : 0.0570 +/- 0.0009 3) pointer    : 0.0061 +/- 0.0004  Results for `uint16` 1) memory view: 0.0806 +/- 0.0002 2) np.ndarray : 0.0882 +/- 0.0005 3) pointer    : 0.0121 +/- 0.0003  Results for `uint32` 1) memory view: 0.0572 +/- 0.0016 2) np.ndarray : 0.0571 +/- 0.0021 3) pointer    : 0.0248 +/- 0.0008  Results for `uint64` 1) memory view: 0.0618 +/- 0.0007 2) np.ndarray : 0.0621 +/- 0.0014 3) pointer    : 0.0481 +/- 0.0006  Results for `float32` 1) memory view: 0.0945 +/- 0.0013 2) np.ndarray : 0.0947 +/- 0.0018 3) pointer    : 0.0942 +/- 0.0020  Results for `float64` 1) memory view: 0.0981 +/- 0.0026 2) np.ndarray : 0.0982 +/- 0.0026 3) pointer    : 0.0968 +/- 0.0016  ASSIGNMENT PERFORMANCE (100,000,000 element array): Results for `uint8` 1) memory view: 0.0341 +/- 0.0010 2) np.ndarray : 0.0476 +/- 0.0007 3) pointer    : 0.0402 +/- 0.0001  Results for `uint16` 1) memory view: 0.0368 +/- 0.0020 2) np.ndarray : 0.0368 +/- 0.0019 3) pointer    : 0.0279 +/- 0.0009  Results for `uint32` 1) memory view: 0.0429 +/- 0.0022 2) np.ndarray : 0.0427 +/- 0.0005 3) pointer    : 0.0418 +/- 0.0007  Results for `uint64` 1) memory view: 0.0833 +/- 0.0004 2) np.ndarray : 0.0835 +/- 0.0011 3) pointer    : 0.0832 +/- 0.0003  Results for `float32` 1) memory view: 0.0648 +/- 0.0061 2) np.ndarray : 0.0644 +/- 0.0044 3) pointer    : 0.0639 +/- 0.0005  Results for `float64` 1) memory view: 0.0854 +/- 0.0056 2) np.ndarray : 0.0849 +/- 0.0043 3) pointer    : 0.0847 +/- 0.0056 

Benchmark Code (Shown only for access+assignment)

# cython: boundscheck=False # cython: wraparound=False # cython: nonecheck=False import numpy as np cimport numpy as np cimport cython  # Change these as desired. data_type = np.uint64 ctypedef np.uint64_t data_type_t  cpdef test_memory_view(data_type_t [:] view):     cdef Py_ssize_t i, j, n = view.shape[0]      for j in range(0, n):         for i in range(0, n):             view[i] = view[j]  cpdef test_ndarray(np.ndarray[data_type_t, ndim=1] view):     cdef Py_ssize_t i, j, n = view.shape[0]      for j in range(0, n):         for i in range(0, n):             view[i] = view[j]  cpdef test_pointer(data_type_t [:] view):     cdef Py_ssize_t i, j, n = view.shape[0]     cdef data_type_t * data_ptr = &view[0]      for j in range(0, n):         for i in range(0, n):             (data_ptr + i)[0] = (data_ptr + j)[0]  def run_test():     import time     from statistics import stdev, mean     n = 10000     repeats = 100     a = np.arange(0, n,  dtype=data_type)     funcs = [('1) memory view', test_memory_view),         ('2) np.ndarray', test_ndarray),         ('3) pointer', test_pointer)]      results = {label: [] for label, func in funcs}     for r in range(0, repeats):         for label, func in funcs:             start=time.time()             func(a)             results[label].append(time.time() - start)      print('Results for `{}`'.format(data_type.__name__))     for label, times in sorted(results.items()):         print('{: <14}: {:.4f} +/- {:.4f}'.format(label, mean(times), stdev(times))) 

These benchmarks indicate that on the whole there is not much difference in performance. Sometimes the np.ndarray notation is a little faster, and sometimes vice-verca.

One thing to watch out for with benchmarks is that when the code is made a little bit more complicated or 'realistic' the difference suddenly vanishes, as if the compiler loses confidence to apply some very clever optimization. This can be seen with the performance of floats where there is no difference whatsoever presumably as some fancy integer optimizations can't be used.

Ease of use

Memory views offer significant advantages, for example you can use a memory view on numpy array, CPython array, cython array, c array and more, both present and future. There is also the simple parallel syntax for casting anything to a memory view:

cdef double [:, :] data_view = <double[:256, :256]>data 

Memory views are great in this regard, because if you type a function as taking a memory view then it can take any of those things. This means you can write a module that doesn't have a dependency on numpy, but which can still take numpy arrays.

On the other hand, np.ndarray notation results in something that is still a numpy array and you can call all the numpy array methods on it. It's not a big deal to have both a numpy array and a view on the array though:

def dostuff(arr):     cdef double [:] arr_view = arr     # Now you can use 'arr' if you want array functions,     # and arr_view if you want fast indexing 

Having both the array and the array view works fine in practise and I quite like the style, as it makes a clear distinction between python-level methods and c-level methods.

Conclusion

Performance is very nearly equal and there is certainly not enough difference for that to be a deciding factor.

The numpy array notation comes closer to the ideal of accelerating python code without changing it much, as you can continue to use the same variable, while gaining full-speed array indexing.

On the other hand, the memory view notation probably is the future. If you like the elegance of it, and use different kinds of data containers than just numpy arrays, there is very good reason for using memory views for consistency's sake.

like image 50
Blake Walsh Avatar answered Sep 29 '22 10:09

Blake Walsh