1

I have following Cython code with one C buffer defined (c_buffer):

ctypedef struct my_struct_t:
    float x
    float y

cdef class CMyClass:
    cdef my_struct_t c_buffer[1000]

    def get_array(self):
        return <my_struct_t[:1000]>&self.c_buffer[0]

    def get_memoryview(self):
        return memoryview(<my_struct_t[:1000]>&self.c_buffer[0])

I'm using this class to store elements that eventually go to OpenGL VBO buffer. What I'm trying to do is avoid unnecessary copies of memory.

When I call get_array() I get result of type

<c_wrappers.array object at 0x7fffce17d650>

with get_memoryview() the result is:

<memory at 0x7fffd242e648>
  1. What's the difference between them (in functionality/speed)? I'm reading official document in about Typed Memoryviews, but it focuses mainly on numpy. Am I returning the memoryviews here correctly?

  2. Now the buffer is fixed (max 1000 elements). Does in Cython exist dynamic array that I could use and it automatically handles memory for me (for adding/removing elements at runtime) and having continuous memory layout (that I could eventually supply to OpenGL VBO)? Or should I use from libcpp.vector cimport vector?

1 Answer 1

2

That is a quite complicated question! There are some aspects that need to be considered.

The speed:

Let's start with a simple int-buffer (I've skipped the unnecessary &c_buffer[0]-business):

%%cython
cdef class CMyClass:
    cdef int c_buffer[1000]

    def get_array(self):
        return <int[:1000]>self.c_buffer

    def get_memoryview(self):
        return memoryview(<int[:1000]>self.c_buffer)

"Typed memory view" is somewhat opaque in Cython, there are some classes which are very similar and are returned from the function depending on the signature of the function:

However, none of these above is the memoryview you are returning in your second-function: it returns Python's memoryview.

Quite confusing! Personally, I keep it simple and trust Cython to return the best suitable class - to me it just a buffer.

When we measure the speed, the first version will be faster, because wrapping array_obj into a Python's memoryview just adds complexity:

>>>c=CMyClass()
>>>%timeit c.get_array()
377 ns ± 1.69 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>>%timeit c.get_memoryview()
561 ns ± 2.31 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)

The lifetime:

The memory from c_buffer isn't copied:

>>>c=CMyClass()
>>>c.get_array()[0]=42
>>>print(c.get_memoryview()[0])

Which sounds like a Good Thing, but isn't! The problem: c_buffer isn't a Python-object and when it goes out of scope, the data-pointer of the memory-view becomes dangling:

>>c=CMyClass()
>>>c.get_array()[0]=42
>>>c=c.get_array()   # old c-object is now destroyed
>>>print(c[0])       # anything can happen!
-304120624

I got lucky, the python didn't crash but it could, because after binding c to the memoryview, the underlying object is destroyed and the memory freed.

Using std::vector will not help you there. What you need is a real Python-object with reference counting! For example we could use Cython's array for that:

%%cython 

from cython.view cimport array as cvarray
cdef class CMyClass:
    cdef int[:] c_buffer

    def __cinit__(self):
        self.c_buffer = cvarray(shape=(1000,), itemsize=sizeof(int), format="i")

    def  get_array(self):
        cdef int[:] res=self.c_buffer # nobody needs to know which class we use
        return res

Now the code from above is safe:

>>c=CMyClass()
>>>c.get_array()[0]=42
>>>c=c.get_array()   # old c-object is now destroyed
>>>print(c[0])       # but the underlying memory is still alive
42

Custom structs:

But what about customs structs, as in your example above? The probably easiest way is to use numpy:

%%cython -a
import numpy as np
cimport numpy as np

#define a type for memory view
ctypedef packed struct my_struct_t:
    np.float32_t x
    np.float32_t y

#define a type for numpy-array (is a python-object)
my_struct = np.dtype([
    ('x', np.float32, 1), 
    ('y', np.float32, 1),  
])

cdef class CMyClass:
    cdef object c_buffer

    def __cinit__(self):
        self.c_buffer = np.empty(1000,dtype=my_struct)

    def  get_array(self):
        cdef my_struct_t[:] res=self.c_buffer
        return res

Which works as advertised:

>>>c=CMyClass()
>>>c.get_array()[0]={'x':42,'y':42}
>>>c=c.get_array()   # old c-object is now destroyed
>>>print(c[0])       # but this is still ok
{'x': 42.0, 'y': 42.0}

Two more remarks:

  • using numpy is slower - get_array() is three times slower than the original get_array() version

  • using my_struct_t c_buffer would not really help you there (beside being dangerous), because there would be no rule how to translate the data from c-struct to an python object, but this check happens at run time, when the elements of the array are acccessed.

Sign up to request clarification or add additional context in comments.

4 Comments

Thanks for exhausting answer! Just few remarks: so "typed memory view" is just a concept and I need to find proper function(malloc, cvarray etc..) to set it? In your example you set cvarray to 1000, what if i want add more elements at runtine? Should I reallocate?
@AndrejKesely I think of "typed memory views" as an abstraction. I don't see much value in known which classes/combination of classes makes it work.
@AndrejKesely you could use array.array, which can grow dynamically: cython.readthedocs.io/en/latest/src/tutorial/array.html. However, if there are memory views around, they lock the underlying buffer, so it cannot be changed.
I see, it's complicated...I want to grab memoryview only when I'm ready to draw the buffer, so that shouldn't be a problem. Thanks for clarifying me the typed memory view.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.