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_UCS2
或 Py_UCS1
.
但是,当使用 Py_UCS2
或 Py_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 字节。
上下文:我构建了一个树数据结构,它在 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_UCS2
或 Py_UCS1
.
但是,当使用 Py_UCS2
或 Py_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 字节。