cython 中 str 和 Py_UNICODE 的实习和内存地址

Interning and memory address for str and Py_UNICODE in cython

上下文:我构建了一个树数据结构,它在 cython 的节点中存储单个字符。现在我想知道如果我实习所有这些角色是否可以节省内存。以及我应该使用 Py_UNICODE 作为变量类型还是常规 str。这是我精简的 Node 对象,使用 Py_UNICODE:

from libc.stdint cimport uintptr_t
from cpython cimport PyObject

cdef class Node():
    cdef:
        public Py_UNICODE character

    def __init__(self, Py_UNICODE character):
        self.character = character

    def memory(self):
        return <uintptr_t>&self.character

如果先尝试看字符是否被自动intern。如果我在 Python 中导入 class 并创建多个具有不同或相同字符的对象,这些是我得到的结果:

a = Node("a")
a_py = a.character
a2 = Node("a")
b = Node("b")

print(a.memory(), a2.memory(), b.memory())
# 140532544296704 140532548558776 140532544296488

print(id(a.character), id(a2.character), id(b.character), id(a_py))
# 140532923573504 140532923573504 140532923840528 140532923573504

所以从那我会得出结论 Py_UNICODE 不会自动实习并且在 python 中使用 id() 不会给我实际的内存地址,而是一个副本的地址(我假设 python 自动实习单个 unicode 字符,然后 returns 给我的内存地址)。

接下来我尝试在使用 str 时做同样的事情。只需将 Py_UNICODE 替换为 str ,这就是我现在尝试的方式:

%%cython
from libc.stdint cimport uintptr_t
from cpython cimport PyObject

cdef class Node():
    cdef:
        public str character

    def __init__(self, str character):
        self.character = character

    def memory(self):
        return <uintptr_t>(<PyObject*>self.character)

这些是我得到的结果:

...
print(a.memory(), a2.memory(), b.memory())
# 140532923573504 140532923573504 140532923840528

print(id(a.character), id(a2.character), id(b.character), id(a_py))
# 140532923573504 140532923573504 140532923840528 140532923573504

基于此,我首先认为单个字符 str 也可以在 cython 中进行实习,并且 cython 不需要从 python 复制字符,这解释了为什么 id() 和 .memory( ) 提供相同的地址。但是后来我尝试使用更长的字符串得到了相同的结果,我可能不想从中得出更长的字符串也会自动被实习的结论?如果我使用 Py_UNICODE,我的树也会使用更少的内存,所以如果 str 被 interned,那没有多大意义,但 Py_UNICODE 不是。有人可以向我解释这种行为吗?我将如何进行实习?

(我正在 Jupyter 中对此进行测试,以防有影响)

编辑:删除了不必要的节点 ID 比较而不是字符。

你这边有误会。 PY_UNICODE 不是 python-object - 它是 typedef for wchar_t.

只有字符串对象(至少其中一些)会被驻留,而不是 wchar_t 类型的简单 C-variables(或者事实上任何 C-type)。它也没有任何意义:wchar_t 很可能是 32 位大,而保持指向内部对象的指针将花费 64 位。

因此,变量self.character(类型PY_UNICODE)的内存地址永远不会相同,只要self是不同的对象(无论哪个值self.character 有).

另一方面,当您在纯 python 中调用 a.character 时,Cython 知道该变量不是简单的 32 位整数并自动将其转换(character 是 属性 对吗?)通过 PyUnicode_FromOrdinal 到 unicode-object。返回的字符串(即 a_py)可能是 "interned" 也可能不是

当这个字符的代码点is less than 256 (i.e. latin1), it gets kind of interned - otherwise not. The first 256 unicode-objects consisting of only one character have a special place - not the same as other interned strings(因此上一节中"interned"的用法)。

考虑:

>>> a="\u00ff" # ord(a) =  255
>>> b="\u00ff"
>>> a is b
# True

但是

>>> a="\u0100" # ord(a) =  256
>>> b="\u0100"
>>> a is b
# False

关键是:使用 PY_UNICODE - 即使没有实习也比实习更便宜(4 字节)strings/unicode-objects(8 字节供参考 + 一次实习的内存object) 并且比没有 interned objects 便宜得多(这可能发生)。

或者更好,正如@user2357112指出的那样,使用Py_UCS4保证4个字节的大小得到保证(需要能够支持所有可能 unicode-characters) - wchar_t 可以小到 1 个字节(即使这在今天可能很不寻常)。如果您对使用的字符了解更多,则可以退回到 Py_UCS2Py_UCS1.


但是,当使用 Py_UCS2Py_USC1 时必须考虑到,Cython 将不支持转换 from/to unicode,就像 Py_UCS4 的情况一样(或弃用 Py_UNICODE) 并且必须手动完成,例如:

%%cython 
from libc.stdint cimport uint16_t

# need to wrap typedef as Cython doesn't do it
cdef extern from "Python.h":
    ctypedef uint16_t Py_UCS2

cdef class Node:
    cdef:
        Py_UCS2 character_

    @property
    def character(self):
        # cython will do the right thing for Py_USC4
        return <Py_UCS4>(self.character_) 

    def __init__(self, str character):
        # unicode -> Py_UCS4 managed by Cython
        # Py_UCS4 -> Py_UCS2 is a simple C-cast
        self.character_ = <Py_UCS2><Py_UCS4>(character)

还应该确保,使用 Py_USC2 确实节省了内存:CPython 使用 pymalloc,它具有 8 个字节的对齐方式,这意味着例如20 字节仍将使用 24 字节(3*8)内存。另一个问题是来自 C-compiler、

的结构对齐
struct A{
    long long int a;
    long long int b;
    char ch;
};

sizeof(A) 是 24 而不是 17(参见 live)。

如果这两个字节之后真的有一个,那么还有一条更大的鱼要炸:不要创建节点 Python-objects 因为它会带来 16 字节的开销,因为实际上不需要多态性和引用计数 -这意味着整个数据结构应该用 C 编写并在 Python 中作为一个整体 wrappend。然而,这里也要确保以正确的方式分配内存:通常的 C-runtime 内存分配器具有 32 或 64 字节对齐,即分配较小的大小仍会导致使用 32/64 字节。