【问题标题】:Cython struct member scopeCython 结构成员范围
【发布时间】:2019-01-06 03:19:19
【问题描述】:

我在 Cython 中有一个使用 char * 成员的数据结构。

发生的情况是成员值似乎失去了在为成员分配值的函数之外的范围。请参阅此示例(使用 IPython):

[nav] In [26]: %%cython -f 
      ...: ctypedef struct A: 
      ...:     char *s 
      ...:      
      ...: cdef char *_s 
      ...:      
      ...: cdef void fn(A *a, msg): 
      ...:     s = msg.encode() 
      ...:     a[0].s = s 
      ...:  
      ...: cdef A _a 
      ...: _a.s = _s 
      ...: fn(&_a, 'hello') 
      ...: print(_a.s)          
      ...: print(b'hola') 
      ...: print(_a.s)          
b'hello'
b'hola'
b"b'hola'"

看起来_a.sfn 之外被释放,并且被分配了内存中适合该插槽的任何垃圾。

这仅在特定情况下发生。例如,如果我将 b'hello' 分配给 s 而不是 fn() 内的编码字符串,则正确的字符串将在函数外部打印。

如您所见,我还为 char 变量添加了一个额外的声明,并在执行 fn 之前将其分配给 struct,以确保 _a.s 指针不会超出范围。但是,我怀疑问题是将成员分配给函数范围内的变量。

这里到底发生了什么,我该如何解决这个问题?

谢谢。

【问题讨论】:

    标签: pointers scope cython


    【解决方案1】:

    您的问题是,指针 a.s 在创建后立即在 fn 函数中悬空。

    当调用msg.encode() 时,会创建临时字节对象s,并将其缓冲区的地址保存到a.s。然而,紧接着(即在函数退出时)临时字节对象被破坏并且指针变得悬空。

    因为字节对象很小,Python's memory manager 在竞技场中管理它的内存——这保证了当你访问地址时没有段错误(你很幸运)。

    虽然临时对象被销毁,但内存并没有被覆盖/清理,因此从A.s 的角度来看,临时对象看起来好像还活着。

    每当您创建一个与临时对象大小相似的新字节对象时,竞技场中的旧内存可能会被重用,因此您的指针a.s 可以指向新分配的字节对象的缓冲区。

    顺便说一句,你会直接使用a[0].s = msg.encode()(我猜你这样做了),Cython 不会构建并告诉你,你试图说对临时 Python 对象的引用。添加显式引用欺骗了 Cython,但对您的情况没有帮助。

    那该怎么办呢?哪种解决方案合适取决于大局,但可用的策略是:

    1. 管理A.s的内存。 IE。手动保留内存,从临时对象中复制,完成后立即释放内存。
    2. 管理引用计数:将PyObject * 添加到A-struct。将临时对象分配给它(不要忘记手动增加引用计数器),完成后立即减少引用计数器。
    3. 将临时对象的引用收集到一个池中(例如列表),这将使它们保持活动状态。不需要对象时立即清除池。

    并不总是最好的,但最简单的是选项 3 - 您不必管理内存而不是引用计数:

    %%cython
    ...
    pool=[]   
    cdef void fn(A *a, msg):    
        s = msg.encode()
        pool.append(s) 
        a[0].s = s
    

    虽然这并不能解决主要问题,但在这种情况下,使用 PyUnicode_AsUTF8(启发 by this answer)可能是一个令人满意的解决方案:

    %%cython
    
    # it is not wrapped by `cpython.pxd`:
    cdef extern from "Python.h":
        const char* PyUnicode_AsUTF8(object unicode)
    ...
    
    cdef void fn(A *a, msg): 
     a[0].s = PyUnicode_AsUTF8(msg) # msg.encode() uses utf-8 as default.
    

    这至少有两个优点:

    • 只要msg 还活着,指针a[0].s 就有效
    • 调用PyUnicode_AsUTF8(msg)msg.encode()快,因为它重用了缓存的缓冲区,所以第一次调用后基本上O(1),而msg.encode()至少需要复制内存并且是O(n)n-字符数。

    【讨论】:

    • 我想避免在这个函数中使用 Python 对象和函数调用(除了必要的输入 msg),因为它是底层的,所以添加一个列表很可能会降低性能。我可能想使用#1,这有点痛苦,因为我的结构有 3 个这样的字符串成员,但可能是最节省内存的解决方案。我从未使用过cymem,但这似乎是一种让内存管理更容易的好方法。
    • 目前,我只是将函数代码移到了调用函数中,因为暂时没有其他人在使用它,但很高兴知道我对未来的选择。
    • 另一种解决方案是让函数返回一个新结构吗?会继续分配吗?
    • @user3758232 在不了解全局和使用分析器的情况下,很难判断什么是“最佳”解决方案。也许最好将数据保存在字节对象中(然后不再需要创建临时对象)而不是 unicode 对象,也许是上述策略之一,也许是别的。
    • @user3758232 我已经用另一个解决方案更新了答案,也许这就是你要找的
    猜你喜欢
    • 2011-07-13
    • 2016-01-18
    • 2019-04-18
    • 1970-01-01
    • 2013-02-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多