Sicherer Weg, einen C-zugewiesenen Speicherpuffer mit numpy/ctypes verfügbar zu machen?

Lesezeit: 18 Minuten

Benutzer-Avatar
ali_m

Ich schreibe Python-Bindungen für eine C-Bibliothek, die gemeinsam genutzte Speicherpuffer verwendet, um ihren internen Zustand zu speichern. Die Zuweisung und Freigabe dieser Puffer erfolgt außerhalb von Python durch die Bibliothek selbst, aber ich kann indirekt steuern, wann dies geschieht, indem ich eingeschlossene Konstruktor-/Destruktorfunktionen innerhalb von Python aufrufe. Ich möchte einige der Puffer für Python verfügbar machen, damit ich aus ihnen lesen und in einigen Fällen Werte an sie übertragen kann. Leistung und Speichernutzung sind wichtige Anliegen, daher möchte ich das Kopieren von Daten nach Möglichkeit vermeiden.

Mein aktueller Ansatz besteht darin, ein numpy-Array zu erstellen, das einen direkten Blick auf einen ctypes-Zeiger bietet:

import numpy as np
import ctypes as C

libc = C.CDLL('libc.so.6')

class MyWrapper(object):

    def __init__(self, n=10):
        # buffer allocated by external library
        addr = libc.malloc(C.sizeof(C.c_int) * n)
        self._cbuf = (C.c_int * n).from_address(addr)

    def __del__(self):
        # buffer freed by external library
        libc.free(C.addressof(self._cbuf))
        self._cbuf = None

    @property
    def buffer(self):
        return np.ctypeslib.as_array(self._cbuf)

Neben der Vermeidung von Kopien bedeutet dies auch, dass ich die Indizierungs- und Zuweisungssyntax von numpy verwenden und direkt an andere numpy-Funktionen übergeben kann:

wrap = MyWrapper()
buf = wrap.buffer       # buf is now a writeable view of a C-allocated buffer

buf[:] = np.arange(10)  # this is pretty cool!
buf[::2] += 10

print(wrap.buffer)
# [10  1 12  3 14  5 16  7 18  9]

Es ist jedoch auch von Natur aus gefährlich:

del wrap                # free the pointer

print(buf)              # this is bad!
# [1852404336 1969367156  538978662  538976288  538976288  538976288
#  1752440867 1763734377 1633820787       8548]

# buf[0] = 99           # uncomment this line if you <3 segfaults

Um dies sicherer zu machen, muss ich überprüfen können, ob der zugrunde liegende C-Zeiger freigegeben wurde, bevor ich versuche, den Array-Inhalt zu lesen/schreiben. Ich habe ein paar Gedanken dazu, wie man das macht:

  • Eine Möglichkeit wäre, eine Unterklasse von zu generieren np.ndarray das enthält einen Verweis auf die _cbuf Attribut von MyWrapperüberprüft, ob dies der Fall ist None vor dem Lesen/Schreiben in den zugrunde liegenden Speicher und löst eine Ausnahme aus, wenn dies der Fall ist.
  • Ich könnte problemlos mehrere Ansichten auf denselben Puffer generieren, z. B. durch .view Casting oder Slicing, also müssten beide den Verweis auf erben _cbuf und die Methode, die die Prüfung durchführt. Ich vermute, dass dies durch Überschreiben erreicht werden könnte __array_finalize__aber ich weiß nicht genau wie.
  • Die “Pointer-Checking”-Methode müsste auch vor jeder Operation aufgerufen werden, die den Inhalt des Arrays lesen und/oder schreiben würde. Ich weiß nicht genug über die Interna von numpy, um eine vollständige Liste der zu überschreibenden Methoden zu haben.

Wie könnte ich eine Unterklasse von implementieren np.ndarray der diese Prüfung durchführt? Kann jemand einen besseren Ansatz vorschlagen?


Aktualisieren: Diese Klasse macht das meiste, was ich will:

class SafeBufferView(np.ndarray):

    def __new__(cls, get_buffer, shape=None, dtype=None):
        obj = np.ctypeslib.as_array(get_buffer(), shape).view(cls)
        if dtype is not None:
            obj.dtype = dtype
        obj._get_buffer = get_buffer
        return obj

    def __array_finalize__(self, obj):
        if obj is None: return
        self._get_buffer = getattr(obj, "_get_buffer", None)

    def __array_prepare__(self, out_arr, context=None):
        if not self._get_buffer(): raise Exception("Dangling pointer!")
        return out_arr

    # this seems very heavy-handed - surely there must be a better way?
    def __getattribute__(self, name):
        if name not in ["__new__", "__array_finalize__", "__array_prepare__",
                        "__getattribute__", "_get_buffer"]:
            if not self._get_buffer(): raise Exception("Dangling pointer!")
        return super(np.ndarray, self).__getattribute__(name)

Zum Beispiel:

wrap = MyWrapper()
sb = SafeBufferView(lambda: wrap._cbuf)
sb[:] = np.arange(10)

print(repr(sb))
# SafeBufferView([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

print(repr(sb[::2]))
# SafeBufferView([0, 2, 4, 6, 8], dtype=int32)

sbv = sb.view(np.double)
print(repr(sbv))
# SafeBufferView([  2.12199579e-314,   6.36598737e-314,   1.06099790e-313,
#          1.48539705e-313,   1.90979621e-313])

# we have to call the destructor method of `wrap` explicitly - `del wrap` won't
# do anything because `sb` and `sbv` both hold references to `wrap`
wrap.__del__()

print(sb)                # Exception: Dangling pointer!
print(sb + 1)            # Exception: Dangling pointer!
print(sbv)               # Exception: Dangling pointer!
print(np.sum(sb))        # Exception: Dangling pointer!
print(sb.dot(sb))        # Exception: Dangling pointer!

print(np.dot(sb, sb))    # oops...
# -70104698

print(np.extract(np.ones(10), sb))
# array([251019024,     32522, 498870232,     32522,         4,         5,
#               6,         7,        48,         0], dtype=int32)

# np.copyto(sb, np.ones(10, np.int32))    # don't try this at home, kids!

Ich bin mir sicher, dass es noch andere Grenzfälle gibt, die ich übersehen habe.


Aktualisierung 2: Ich habe mit herumgespielt weakref.proxywie vorgeschlagen von @ivan_pozdeev. Es ist eine nette Idee, aber leider kann ich nicht sehen, wie es mit numpy Arrays funktionieren würde. Ich könnte versuchen, eine Weakref für das von zurückgegebene numpy-Array zu erstellen .buffer:

wrap = MyWrapper()
wr = weakref.proxy(wrap.buffer)
print(wr)
# ReferenceError: weakly-referenced object no longer exists
# <weakproxy at 0x7f6fe715efc8 to NoneType at 0x91a870>

Ich denke, das Problem hier ist, dass die np.ndarray Instanz zurückgegeben von wrap.buffer geht sofort aus dem Rahmen. Eine Problemumgehung wäre, dass die Klasse das Array bei der Initialisierung instanziiert, einen starken Verweis darauf hält und die .buffer() Getter-Rückkehr a weakref.proxy zum Array:

class MyWrapper2(object):

    def __init__(self, n=10):
        # buffer allocated by external library
        addr = libc.malloc(C.sizeof(C.c_int) * n)
        self._cbuf = (C.c_int * n).from_address(addr)
        self._buffer = np.ctypeslib.as_array(self._cbuf)

    def __del__(self):
        # buffer freed by external library
        libc.free(C.addressof(self._cbuf))
        self._cbuf = None
        self._buffer = None

    @property
    def buffer(self):
        return weakref.proxy(self._buffer)

Dies wird jedoch unterbrochen, wenn ich eine zweite Ansicht auf dasselbe Array erstelle, während der Puffer noch zugewiesen ist:

wrap2 = MyWrapper2()
buf = wrap2.buffer
buf[:] = np.arange(10)

buf2 = buf[:]   # create a second view onto the contents of buf

print(repr(buf))
# <weakproxy at 0x7fec3e709b50 to numpy.ndarray at 0x210ac80>
print(repr(buf2))
# array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

wrap2.__del__()

print(buf2[:])  # this is bad
# [1291716568    32748 1291716568    32748        0        0        0
#         0       48        0] 

print(buf[:])   # WTF?!
# [34525664        0        0        0        0        0        0        0
#         0        0]  

Das ist Ernsthaft kaputt – nach dem Anruf wrap2.__del__() Ich kann nicht nur lesen und schreiben buf2 das war eine numpy-Array-Ansicht auf wrap2._cbufaber ich kann sogar lesen und schreiben bufwas damit nicht möglich sein sollte wrap2.__del__() setzt wrap2._buffer zu None.

  • Haben Sie versucht, einen Wrapper mit Cython zu schreiben? Es hat eine sauberere (und wahrscheinlich sicherere) Schnittstelle zum Abrufen von Ansichten von Speicherpuffern über “typisierte Speicheransichten”.

    – JoshAdel

    23. Juni 2016 um 11:16 Uhr

  • @JoshAdel Würden getippte Speicheransichten in diesem Fall wirklich helfen? Letztendlich besteht das Problem darin, dass die Zuweisung und Freigabe dieser Puffer außerhalb von Python durch eine externe Bibliothek erfolgt, über die ich keine direkte Kontrolle habe. Das Beste, was ich tun kann, ist zu verfolgen, ob sie sollen noch zuzuweisen, basierend auf den Bibliotheksfunktionen, die ich bisher aufgerufen habe. Ich nehme an, ich könnte meine Buchhaltung in Cython statt in Python führen, aber ich sehe noch keinen überzeugenden Vorteil darin, zu Cython zu wechseln (es gibt einige andere Gründe, warum dies zu diesem Zeitpunkt schmerzhaft wäre).

    – ali_m

    23. Juni 2016 um 11:35 Uhr


  • Wenn Sie einen starken Verweis auf ein Objekt beibehalten, das den Deleter für Sie aufruft (z cffi (was du solltest stets verwenden statt ctypes) hat eingebaute Unterstützung für einen Deleter mit der gc -Methode), müssen Sie sich keine Gedanken über entwertete schwache Referenzen machen.

    – o11c

    24. Juni 2016 um 3:24 Uhr

  • @o11c gc ist hier irrelevant, da die Puffer außerhalb von Python zugewiesen und freigegeben werden, indem die Bibliothek, die ich umschließe, extern ist.

    – ali_m

    24. Juni 2016 um 7:43 Uhr

  • @ali_m: Zuweisen _buffer = None befreit nicht _buffer, da das andere Array immer noch einen Verweis darauf hat. Wenn Sie manuell eine Funktion aufrufen, die Ihren Zeiger freigibt, bevor Ihr Zeiger freigegeben werden kann, wird das Zeug kaputt gehen.

    – Benutzer2357112

    28. Juni 2016 um 21:26 Uhr

Sie müssen einen Verweis auf Ihren Wrapper behalten, während irgendein numpy-Array existiert. Der einfachste Weg, dies zu erreichen, besteht darin, diese Referenz in einem Attribut des ctype-buffers zu speichern:

class MyWrapper(object):
    def __init__(self, n=10):
        # buffer allocated by external library
        self.size = n
        self.addr = libc.malloc(C.sizeof(C.c_int) * n)

    def __del__(self):
        # buffer freed by external library
        libc.free(self.addr)

    @property
    def buffer(self):
        buf = (C.c_int * self.size).from_address(self.addr)
        buf._wrapper = self
        return np.ctypeslib.as_array(buf)

Auf diese Weise wird Ihr Wrapper automatisch freigegeben, wenn die letzte Referenz, zB das letzte numpy-Array, der Garbage Collection unterzogen wird.

Es ist eine proprietäre Bibliothek, die von einem Drittanbieter geschrieben und als Binärdatei vertrieben wird. Ich könnte dieselben Bibliotheksfunktionen von C statt von Python aufrufen, aber das würde nicht viel helfen, da ich immer noch keinen Zugriff auf den Code habe, der die Puffer tatsächlich zuweist und freigibt. Ich kann zum Beispiel die Puffer nicht selbst zuweisen und sie dann als Zeiger an die Bibliothek übergeben.

Sie könnten den Puffer jedoch in einen Python-Erweiterungstyp einschließen. Auf diese Weise können Sie nur die Schnittstelle verfügbar machen, die verfügbar sein soll, und den Erweiterungstyp automatisch die Freigabe des Puffers vornehmen lassen. Auf diese Weise ist es der Python-API nicht möglich, im freien Speicher zu lesen/schreiben.


mybuffer.c

#include <python3.3/Python.h>

// Hardcoded values
// N.B. Most of these are only needed for defining the view in the Python
// buffer protocol
static long external_buffer_size = 32;          // Size of buffer in bytes
static long external_buffer_shape[] = { 32 };   // Number of items for each dimension
static long external_buffer_strides[] = { 1 };  // Size of item for each dimension

//----------------------------------------------------------------------------
// Code to simulate the third-party library
//----------------------------------------------------------------------------

// Allocate a new buffer
static void* external_buffer_allocate()
{
    // Allocate the memory
    void* ptr = malloc(external_buffer_size);

    // Debug
    printf("external_buffer_allocate() = 0x%lx\n", (long) ptr);

    // Fill buffer with a recognizable pattern
    int i;
    for (i = 0; i < external_buffer_size; ++i)
    {
        *((char*) ptr + i) = i;
    }

    // Done
    return ptr;
}

// Free an existing buffer
static void external_buffer_free(void* ptr)
{
    // Debug
    printf("external_buffer_free(0x%lx)\n", (long) ptr);

    // Release the memory
    free(ptr);
}


//----------------------------------------------------------------------------
// Define a new Python instance object for the external buffer
// See: https://docs.python.org/3/extending/newtypes.html
//----------------------------------------------------------------------------

typedef struct
{
    // Python macro to include standard members, like reference count
    PyObject_HEAD

    // Base address of allocated memory
    void* ptr;
} BufferObject;


//----------------------------------------------------------------------------
// Define the instance methods for the new object
//----------------------------------------------------------------------------

// Called when there are no more references to the object
static void BufferObject_dealloc(BufferObject* self)
{
    external_buffer_free(self->ptr);
}

// Called when we want a new view of the buffer, using the buffer protocol
// See: https://docs.python.org/3/c-api/buffer.html
static int BufferObject_getbuffer(BufferObject *self, Py_buffer *view, int flags)
{
    // Set the view info
    view->obj = (PyObject*) self;
    view->buf = self->ptr;                      // Base pointer
    view->len = external_buffer_size;           // Length
    view->readonly = 0;
    view->itemsize = 1;
    view->format = "B";                         // unsigned byte
    view->ndim = 1;
    view->shape = external_buffer_shape;
    view->strides = external_buffer_strides;
    view->suboffsets = NULL;
    view->internal = NULL;

    // We need to increase the reference count of our buffer object here, but
    // Python will automatically decrease it when the view goes out of scope
    Py_INCREF(self);

    // Done
    return 0;
}

//----------------------------------------------------------------------------
// Define the struct required to implement the buffer protocol
//----------------------------------------------------------------------------

static PyBufferProcs BufferObject_as_buffer =
{
    // Create new view
    (getbufferproc) BufferObject_getbuffer,

    // Release an existing view
    (releasebufferproc) 0,
};


//----------------------------------------------------------------------------
// Define a new Python type object for the external buffer
//----------------------------------------------------------------------------

static PyTypeObject BufferType =
{
    PyVarObject_HEAD_INIT(NULL, 0)
    "external buffer",                  /* tp_name */
    sizeof(BufferObject),               /* tp_basicsize */
    0,                                  /* tp_itemsize */
    (destructor) BufferObject_dealloc,  /* tp_dealloc */
    0,                                  /* tp_print */
    0,                                  /* tp_getattr */
    0,                                  /* tp_setattr */
    0,                                  /* tp_reserved */
    0,                                  /* tp_repr */
    0,                                  /* tp_as_number */
    0,                                  /* tp_as_sequence */
    0,                                  /* tp_as_mapping */
    0,                                  /* tp_hash  */
    0,                                  /* tp_call */
    0,                                  /* tp_str */
    0,                                  /* tp_getattro */
    0,                                  /* tp_setattro */
    &BufferObject_as_buffer,            /* tp_as_buffer */
    Py_TPFLAGS_DEFAULT,                 /* tp_flags */
    "External buffer",                  /* tp_doc */
    0,                                  /* tp_traverse */
    0,                                  /* tp_clear */
    0,                                  /* tp_richcompare */
    0,                                  /* tp_weaklistoffset */
    0,                                  /* tp_iter */
    0,                                  /* tp_iternext */
    0,                                  /* tp_methods */
    0,                                  /* tp_members */
    0,                                  /* tp_getset */
    0,                                  /* tp_base */
    0,                                  /* tp_dict */
    0,                                  /* tp_descr_get */
    0,                                  /* tp_descr_set */
    0,                                  /* tp_dictoffset */
    (initproc) 0,                       /* tp_init */
    0,                                  /* tp_alloc */
    0,                                  /* tp_new */
};


//----------------------------------------------------------------------------
// Define a Python function to put in the module which creates a new buffer
//----------------------------------------------------------------------------

static PyObject* mybuffer_create(PyObject *self, PyObject *args)
{
    BufferObject* buf = (BufferObject*)(&BufferType)->tp_alloc(&BufferType, 0);
    buf->ptr = external_buffer_allocate();
    return (PyObject*) buf;
}


//----------------------------------------------------------------------------
// Define the set of all methods which will be exposed in the module
//----------------------------------------------------------------------------

static PyMethodDef mybufferMethods[] =
{
    {"create", mybuffer_create, METH_VARARGS, "Create a buffer"},
    {NULL, NULL, 0, NULL}        /* Sentinel */
};


//----------------------------------------------------------------------------
// Define the module
//----------------------------------------------------------------------------

static PyModuleDef mybuffermodule = {
    PyModuleDef_HEAD_INIT,
    "mybuffer",
    "Example module that creates an extension type.",
    -1,
    mybufferMethods
    //NULL, NULL, NULL, NULL, NULL
};


//----------------------------------------------------------------------------
// Define the module's entry point
//----------------------------------------------------------------------------

PyMODINIT_FUNC PyInit_mybuffer(void)
{
    PyObject* m;

    if (PyType_Ready(&BufferType) < 0)
        return NULL;

    m = PyModule_Create(&mybuffermodule);
    if (m == NULL)
        return NULL;

    return m;
}

test.py

#!/usr/bin/env python3

import numpy as np
import mybuffer

def test():

    print('Create buffer')
    b = mybuffer.create()

    print('Print buffer')
    print(b)

    print('Create memoryview')
    m = memoryview(b)

    print('Print memoryview shape')
    print(m.shape)

    print('Print memoryview format')
    print(m.format)

    print('Create numpy array')
    a = np.asarray(b)

    print('Print numpy array')
    print(repr(a))

    print('Change every other byte in numpy')
    a[::2] += 10

    print('Print numpy array')
    print(repr(a))

    print('Change first byte in memory view')
    m[0] = 42

    print('Print numpy array')
    print(repr(a))

    print('Delete buffer')
    del b

    print('Delete memoryview')
    del m

    print('Delete numpy array - this is the last ref, so should free memory')
    del a

    print('Memory should be free before this line')

if __name__ == '__main__':
    test()

Beispiel

$ gcc -fPIC -shared -o mybuffer.so mybuffer.c -lpython3.3m
$ ./test.py
Create buffer
external_buffer_allocate() = 0x290fae0
Print buffer
<external buffer object at 0x7f7231a2cc60>
Create memoryview
Print memoryview shape
(32,)
Print memoryview format
B
Create numpy array
Print numpy array
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15, 16,
       17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31], dtype=uint8)
Change every other byte in numpy
Print numpy array
array([10,  1, 12,  3, 14,  5, 16,  7, 18,  9, 20, 11, 22, 13, 24, 15, 26,
       17, 28, 19, 30, 21, 32, 23, 34, 25, 36, 27, 38, 29, 40, 31], dtype=uint8)
Change first byte in memory view
Print numpy array
array([42,  1, 12,  3, 14,  5, 16,  7, 18,  9, 20, 11, 22, 13, 24, 15, 26,
       17, 28, 19, 30, 21, 32, 23, 34, 25, 36, 27, 38, 29, 40, 31], dtype=uint8)
Delete buffer
Delete memoryview
Delete numpy array - this is the last ref, so should free memory
external_buffer_free(0x290fae0)
Memory should be free before this line

Ich mochte den Ansatz von @Vikas, aber als ich es versuchte, bekam ich nur ein Numpy-Objekt-Array von einem einzigen FreeOnDel Objekt. Folgendes ist viel einfacher und funktioniert:

class FreeOnDel(object):
    def __init__(self, data, shape, dtype, readonly=False):
        self.__array_interface__ = {"version": 3,
                                    "typestr": numpy.dtype(dtype).str,
                                    "data": (data, readonly),
                                    "shape": shape}
    def __del__(self):
        data = self.__array_interface__["data"][0]      # integer ptr
        print("do what you want with the data at {}".format(data))

view = numpy.array(FreeOnDel(ptr, shape, dtype), copy=False)

wo ptr ist ein Zeiger auf die Daten als ganze Zahl (zB ctypesptr.addressof(...)).

Dies __array_interface__ Das Attribut reicht aus, um Numpy mitzuteilen, wie ein Speicherbereich in ein Array umgewandelt werden soll, und dann das FreeOnDel Objekt wird dieses Array base. Wenn das Array gelöscht wird, wird die Löschung an die weitergegeben FreeOnDel Objekt, wo Sie anrufen können libc.free.

Ich könnte das sogar nennen FreeOnDel Klasse “BufferOwner“, denn das ist seine Aufgabe: die Eigentümerschaft nachzuverfolgen.

Benutzer-Avatar
ivan_pozdeev

weakref ist ein integrierter Mechanismus für die von Ihnen vorgeschlagene Funktionalität. Speziell, weakref.proxy ist ein Objekt mit derselben Schnittstelle wie das referenzierte. Nach der Entsorgung des referenzierten Objekts wird jede Operation auf dem Proxy ausgelöst weakref.ReferenceError. Du brauchst nicht einmal numpy:

In [2]: buffer=(c.c_int*100)()   #acts as an example for an externally allocated buffer
In [3]: voidp=c.addressof(buffer)

In [10]: a=(c.c_int*100).from_address(voidp) # python object accessing the buffer.
                 # Here it's created from raw address value. It's better to use function
                 # prototypes instead for some type safety.
In [14]: ra=weakref.proxy(a)

In [15]: a[1]=1
In [16]: ra[1]
Out[16]: 1

In [17]: del a
In [18]: ra[1]
ReferenceError: weakly-referenced object no longer exists

In [20]: buffer[1]
Out[20]: 1

Wie Sie sehen, benötigen Sie in jedem Fall ein normales Python-Objekt über dem C-Puffer. Besitzt eine externe Bibliothek den Speicher, muss das Objekt gelöscht werden, bevor der Puffer auf C-Ebene freigegeben wird. Wenn Sie den Speicher selbst besitzen, erstellen Sie einfach eine ctypes Objekt auf die übliche Weise, dann wird es freigegeben, wenn es gelöscht wird.

Wenn also Ihre externe Bibliothek den Speicher besitzt und jederzeit freigeben kann (Ihre Spezifikation ist diesbezüglich vage), muss sie Ihnen irgendwie mitteilen, dass dies im Begriff ist – andernfalls haben Sie keine Möglichkeit, dies zu erfahren, um die erforderlichen Maßnahmen zu ergreifen.

Benutzer-Avatar
Vikas

Sie benötigen lediglich eine Hülle mit zusätzlichem __del__ Funktion, bevor Sie sie an die übergeben numpy.ctypeslib.as_array Methode.

class FreeOnDel(object):
    def __init__(self, ctypes_ptr):
        # This is not needed if you are dealing with ctypes.POINTER() objects
        # Start of hack for ctypes ARRAY type;
        if not hasattr(ctypes_ptr, 'contents'):
            # For static ctypes arrays, the length and type are stored
            # in the type() rather than object. numpy queries these 
            # properties to find out the shape and type, hence needs to be 
            # copied. I wish type() properties could be automated by 
            # __getattr__ too
            type(self)._length_ = type(ctypes_ptr)._length_
            type(self)._type_ = type(ctypes_ptr)._type_
        # End of hack for ctypes ARRAY type;

        # cannot call self._ctypes_ptr = ctypes_ptr because of recursion
        super(FreeOnDel, self).__setattr__('_ctypes_ptr', ctypes_ptr)

    # numpy.ctypeslib.as_array function sets the __array_interface__
    # on type(ctypes_ptr) which is not called by __getattr__ wrapper
    # Hence this additional wrapper.
    @property
    def __array_interface__(self):
        return self._ctypes_ptr.__array_interface__

    @__array_interface__.setter
    def __array_interface__(self, value):
        self._ctypes_ptr.__array_interface__ = value

    # This is the onlly additional function we need rest all is overhead
    def __del__(self):
        addr = ctypes.addressof(self._ctypes_ptr)
        print("freeing address %x" % addr)
        libc.free(addr)
        # Need to be called on all object members
        # object.__del__(self) does not work
        del self._ctypes_ptr

    def __getattr__(self, attr):
        return getattr(self._ctypes_ptr, attr)

    def __setattr__(self, attr, val):
        setattr(self._ctypes_ptr, attr, val)

Zu testen

In [32]: import ctypes as C

In [33]: n = 10

In [34]: libc = C.CDLL("libc.so.6")

In [35]: addr = libc.malloc(C.sizeof(C.c_int) * n)

In [36]: cbuf = (C.c_int * n).from_address(addr)

In [37]: wrap = FreeOnDel(cbuf)

In [38]: sb = np.ctypeslib.as_array(wrap, (10,))

In [39]: sb[:] = np.arange(10)

In [40]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

In [41]: print(repr(sb[::2]))
array([0, 2, 4, 6, 8], dtype=int32)

In [42]: sbv = sb.view(np.double)

In [43]: print(repr(sbv))
array([  2.12199579e-314,   6.36598737e-314,   1.06099790e-313,
         1.48539705e-313,   1.90979621e-313])

In [45]: buf2 = sb[:8]

In [46]: sb[::2] += 10

In [47]: del cbuf   # Memory not freed because this does not have __del__

In [48]: del wrap   # Memory not freed because sb, sbv, buf2 have references

In [49]: del sb     # Memory not freed because sbv, buf have references

In [50]: del buf2   # Memory not freed because sbv has reference

In [51]: del sbv    # Memory freed because no more references
freeing address 2bc6bc0

Tatsächlich ist eine einfachere Lösung das Überschreiben __del__ Funktion

In [7]: olddel = getattr(cbuf, '__del__', lambda: 0)

In [8]: cbuf.__del__ = lambda self : libc.free(C.addressof(self)), olddel

In [10]: import numpy as np

In [12]: sb = np.ctypeslib.as_array(cbuf, (10,))

In [13]: sb[:] = np.arange(10)

In [14]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

In [15]: print(repr(sb))
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], dtype=int32)

In [16]: print(repr(sb[::2]))
array([0, 2, 4, 6, 8], dtype=int32)

In [17]: sbv = sb.view(np.double)

In [18]: print(repr(sbv))
array([  2.12199579e-314,   6.36598737e-314,   1.06099790e-313,
         1.48539705e-313,   1.90979621e-313])

In [19]: buf2 = sb[:8]

In [20]: sb[::2] += 10

In [22]: del cbuf   # Memory not freed

In [23]: del sb     # Memory not freed because sbv, buf have references

In [24]: del buf2   # Memory not freed because sbv has reference

In [25]: del sbv    # Memory freed because no more references

Wenn Sie die Lebensdauer des C-Puffers vollständig von Python aus steuern können, haben Sie im Wesentlichen ein Python-„Puffer“-Objekt, das an ndarray sollte nutzen.

Daher,

  • Es gibt zwei grundlegende Möglichkeiten, sie zu verbinden:
    • Puffer -> ndarray
    • ndarray -> Puffer
  • Es stellt sich auch die Frage, wie der Puffer selbst implementiert werden soll

Puffer -> ndarray

Ist unsicher: Es gibt nichts, was automatisch einen Verweis darauf enthält buffer für die Lebenszeit von ndarray. Es ist nicht besser, ein drittes Objekt einzuführen, um Verweise auf beide zu enthalten: Dann müssen Sie nur das dritte Objekt im Auge behalten, anstatt das buffer.

ndarray -> Puffer

“Jetzt redest du!” Da die eigentliche Aufgabe darin besteht, “Puffer, dass ein ndarray verwenden sollten”? Dies ist der natürliche Weg.

In der Tat, numpy hat einen eingebauten Mechanismus: beliebig ndarray das seinen Speicher nicht besitzt, enthält einen Verweis auf das Objekt, das ihn besitzt base -Attribut (wodurch verhindert wird, dass letzteres von der Garbage Collection erfasst wird). Bei Ansichten wird das Attribut automatisch entsprechend zugewiesen (dem übergeordneten Objekt, falls dessen base ist None oder zu den Eltern base).

Der Haken ist, dass Sie dort nicht einfach irgendein altes Objekt platzieren können. Stattdessen wird das Attribut von einem Konstruktor gefüllt und das vorgeschlagene Objekt zunächst auf die Probe gestellt.

Wenn wir also nur ein benutzerdefiniertes Objekt erstellen könnten numpy.array akzeptiert und hält die Wiederverwendung des Speichers für möglich (numpy.ctypeslib.as_array ist eigentlich ein Wrapper für numpy.array(copy=False) mit ein paar Gesundheitschecks) …

<...>

1370280cookie-checkSicherer Weg, einen C-zugewiesenen Speicherpuffer mit numpy/ctypes verfügbar zu machen?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy