31 votes

Un moyen plus sûr d'exposer un tampon mémoire alloué en C en utilisant numpy/ctypes ?

J'écris des liens Python pour une bibliothèque C qui utilise des tampons de mémoire partagée pour stocker son état interne. L'allocation et la libération de ces tampons sont effectuées en dehors de Python par la bibliothèque elle-même, mais je peux indirectement contrôler le moment où cela se produit en appelant les fonctions constructrices/destructrices enveloppées depuis Python. J'aimerais exposer certains de ces tampons à Python afin de pouvoir les lire et, dans certains cas, leur transmettre des valeurs. Les performances et l'utilisation de la mémoire étant des préoccupations importantes, j'aimerais éviter de copier des données dans la mesure du possible.

Mon approche actuelle consiste à créer un tableau numpy qui fournit une vue directe sur un pointeur ctypes :

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)

En plus d'éviter les copies, cela signifie également que je peux utiliser la syntaxe d'indexation et d'affectation de numpy et la passer directement à d'autres fonctions numpy :

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]

Cependant, elle est aussi intrinsèquement dangereuse :

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

Pour rendre cela plus sûr, je dois pouvoir vérifier si le pointeur C sous-jacent a été libéré avant d'essayer de lire/écrire le contenu du tableau. J'ai quelques idées sur la façon de le faire :

  • Une façon de faire serait de générer une sous-classe de np.ndarray qui contient une référence à la _cbuf l'attribut de MyWrapper vérifie s'il est None avant d'effectuer toute lecture/écriture dans sa mémoire sous-jacente, et lève une exception si c'est le cas.
  • Je pourrais facilement générer plusieurs vues sur le même tampon, par exemple en .view casting ou slicing, donc chacun d'entre eux devra hériter de la référence à _cbuf et la méthode qui effectue le contrôle. Je pense que l'on pourrait y parvenir en surchargeant la méthode __array_finalize__ mais je ne sais pas exactement comment.
  • La méthode de "pointer-checking" devrait également être appelée avant toute opération de lecture et/ou d'écriture du contenu du tableau. Je ne connais pas assez les internes de numpy pour avoir une liste exhaustive des méthodes à surcharger.

Comment pourrais-je implémenter une sous-classe de np.ndarray qui effectue cette vérification ? Quelqu'un peut-il suggérer une meilleure approche ?


Mise à jour : Cette classe fait la plupart de ce que je veux :

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)

Par exemple :

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!

Je suis sûr qu'il y a d'autres cas limites que j'ai manqués.


Mise à jour 2 : Je me suis amusé avec weakref.proxy comme le suggère @ivan_pozdeev . C'est une bonne idée, mais malheureusement je ne vois pas comment cela pourrait fonctionner avec les tableaux numpy. Je pourrais essayer de créer une référence faible vers le tableau numpy retourné par .buffer :

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

Je pense que le problème ici est que le np.ndarray retournée par wrap.buffer sort immédiatement du champ d'application. Une solution de contournement serait que la classe instancie le tableau lors de l'initialisation, qu'elle conserve une référence forte à ce tableau et que la fonction .buffer() getter retourne un weakref.proxy au tableau :

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)

Cependant, cela s'arrête si je crée une deuxième vue sur le même tableau alors que le tampon est toujours alloué :

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]  

Esto es sérieusement cassé - après avoir appelé wrap2.__del__() non seulement je peux lire et écrire sur buf2 qui était une vue d'un tableau numpy sur wrap2._cbuf mais je peux même lire et écrire à buf ce qui ne devrait pas être possible étant donné que wrap2.__del__() fixe wrap2._buffer a None .

0voto

ivan_pozdeev Points 2233

Si vous pouvez contrôler complètement la durée de vie du tampon C à partir de Python, vous disposez essentiellement d'un objet "tampon" Python qu'un utilisateur de Python peut utiliser. ndarray devrait utiliser.

Ainsi,

  • il y a 2 façons fondamentales de les connecter :
    • tampon -> ndarray
    • ndarray -> tampon
  • il y a aussi une question sur la façon d'implémenter le tampon lui-même.

tampon -> ndarray

n'est pas sûr : il n'y a rien qui maintienne automatiquement une référence à buffer pendant toute la durée de vie de ndarray . L'introduction d'un 3ème objet pour contenir les références aux deux n'est pas mieux : il suffit alors de garder la trace du 3ème objet au lieu du buffer .

ndarray -> tampon

"Maintenant tu parles !" Puisque la tâche à accomplir est de "tamponner qu'un ndarray devrait utiliser" ? c'est la voie naturelle à suivre.

En fait, numpy dispose d'un mécanisme intégré : toute ndarray qui ne possède pas sa propre mémoire détient une référence à l'objet qui la possède dans sa mémoire. base (empêchant ainsi la collecte des déchets). Pour les vues, l'attribut est automatiquement assigné en conséquence (à l'objet parent si l'attribut base es None ou chez le parent base ).

Le problème est que vous ne pouvez pas y placer n'importe quel objet. Au lieu de cela, l'attribut est rempli par un constructeur et l'objet suggéré est d'abord soumis à son contrôle.

Donc, si seulement nous pouvions construire un objet personnalisé qui numpy.array accepte et considère comme éligible à la réutilisation de la mémoire ( numpy.ctypeslib.as_array est en fait une enveloppe pour numpy.array(copy=False) avec quelques vérifications de bon sens)...

<...>

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X