与sys.getsizeof`

时间:2018-07-16 21:54:53

标签: python string python-3.x pandas

对于长度为1的Python sys.getsizeof(),为什么str比长度为2的字符串大? (对于长度> 2,该关系似乎按预期方式单调增加。)

示例:

>>> from string import ascii_lowercase
>>> import sys

>>> strings = [ascii_lowercase[:i] for i, _ in enumerate(ascii_lowercase, 1)]
>>> strings
['a',
 'ab',
 'abc',
 'abcd',
 'abcde',
 'abcdef',
 'abcdefg',
 # ...

>>> sizes = dict(enumerate(map(sys.getsizeof, strings), 1))
>>> sizes
{1: 58,   # <--- ??
 2: 51,
 3: 52,
 4: 53,
 5: 54,
 6: 55,
 7: 56,
 8: 57,
 9: 58,
 10: 59,
 11: 60,
 12: 61,
 13: 62,
 14: 63,
 15: 64,
 16: 65,
 # ...

似乎与str.__sizeof__有关,但是我完全不了解C,无法深入了解这种情况下的情况。

3 个答案:

答案 0 :(得分:4)

Python 3.3+'s str is quite a complicated structure,并且最终可以用多达三种不同的方式存储基础数据,具体取决于字符串所使用的API和字符串所表示的代码点。最常见的替代表示形式是缓存的UTF-8表示形式,但这仅适用于非ASCII字符串,因此不适用于此处。

在这种情况下,我怀疑单个字符串(作为实现细节,是一个单例)的使用方式触发了创建旧式wchar_t*表示形式(使用{{3}的扩展名) }可能会导致这种情况),而您的Python构建使用四个字节的wchar_t,导致该字符串比否则大8个字节(a本身为四个字节,{{ 1}}终止符)。这是一个单例这一事实意味着,即使您可能从未触发过这样的旧式API调用,任何检索到对该单例的引用的扩展都会通过将其与旧式结合使用来影响每个人的观察大小API。

就我个人而言,我在Linux 3.6.5安装中根本不复制(大小平滑增加),表示没有创建NUL表示形式,而在Windows 3.6.3安装中{{1} }只有54个字节,而不是58个(与Windows的本机两个字节wchar_t相匹配)。在两种情况下,我都使用'a'运行;可能会有不同版本的wchar_t依赖关系导致您(以及我)不一致的观察结果。

需要明确的是,这笔额外费用并不重要。由于单个字符串是一个单例,因此使用的增量成本实际上仅为4-8个字节(取决于指针宽度)。如果少数字符串最终与旧版API一起使用,则不会浪费内存。

答案 1 :(得分:4)

当您import pandas时,它会执行大量的NumPy任务,包括在所有单ASCII字母字符串上调用UNICODE_setitem,并且大概在其他地方对单ASCII-字母执行类似的操作,数字字符串。

该NumPy函数调用已弃用的C API PyUnicode_AsUnicode

当您在CPython 3.3+中调用该函数时,会将wchar_t *表示形式作为两个wchar_t值wstr和{{1 }},在32位w'a'版本的Python上占用8个字节。 '\0'考虑到了这一点。

因此,所有用于ASCII字母和数字的单字符内联字符串(但没有别的)最终都增加了8个字节。


首先,我们知道这显然是在wchar_t上发生的(每Brad Solomon's answer。)它可能会在str.__size__上发生(miradulo发布,但随后删除了对此的评论在ShadowRanger's answer上),但绝对不在import pandas上。

第二,我们知道发生在np.set_printoptions(precision=4, threshold=625, edgeitems=10)上,但是其他单字符字符串呢?

为了验证前者并测试后者,我运行了以下代码:

import numpy

在多个CPython安装上(但在Linux或macOS上为所有64位CPython 3.4或更高版本),我得到的结果是相同的:

'a'

因此,import sys strings = [chr(i) for i in (0, 10, 17, 32, 34, 47, 48, 57, 58, 64, 65, 90, 91, 96, 97, 102, 103, 122, 123, 130, 0x0222, 0x12345)] sizes = {c: sys.getsizeof(c) for c in strings} print(sizes) import numpy as np sizes = {c: sys.getsizeof(c) for c in strings} print(sizes) np.set_printoptions(precision=4, threshold=625, edgeitems=10) sizes = {c: sys.getsizeof(c) for c in strings} print(sizes) import pandas sizes = {c: sys.getsizeof(c) for c in strings} print(sizes) 不变,{'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 50, '9': 50, ':': 50, '@': 50, 'A': 50, 'Z': 50, '[': 50, '`': 50, 'a': 50, 'f': 50, 'g': 50, 'z': 50, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80} {'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 50, '9': 50, ':': 50, '@': 50, 'A': 50, 'Z': 50, '[': 50, '`': 50, 'a': 50, 'f': 50, 'g': 50, 'z': 50, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80} {'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 50, '9': 50, ':': 50, '@': 50, 'A': 50, 'Z': 50, '[': 50, '`': 50, 'a': 50, 'f': 50, 'g': 50, 'z': 50, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80} {'\x00': 50, '\n': 50, '\x11': 50, ' ': 50, '"': 50, '/': 50, '0': 58, '9': 58, ':': 50, '@': 50, 'A': 58, 'Z': 58, '[': 50, '`': 50, 'a': 58, 'f': 58, 'g': 58, 'z': 58, '{': 50, '\x82': 74, 'Ȣ': 76, '': 80} 也不变(大概是miradulo删除评论的原因……),但是import numpy不变。

它显然会影响ASCII数字和字母,但没有其他影响。

此外,如果将所有set_printoptions更改为import pandas,则字符串永远不会被编码为输出,您将得到相同的结果,这意味着要么与缓存UTF- 8,或者是,但是即使我们不强迫它也一直在发生。


很明显的可能性是,无论Pandas调用什么,都使用legacy PyUnicode API之一为所有ASCII数字和字母生成单字符字符串。因此,这些字符串最终不是以紧凑ASCII格式出现,而是以旧版就绪格式出现,对吗? (有关其含义的详细信息,请参见the comments in the source。)

不。使用我的superhackyinternals中的代码,我们可以看到它仍然是紧凑的ascii格式:

print

我们可以看到Pandas将大小从50更改为58,但字段仍然是:

print(sizes.values())

…换句话说,它是import ctypes import sys from internals import PyUnicodeObject s = 'a' print(sys.getsizeof(s)) ps = PyUnicodeObject.from_address(s) print(ps, ps.kind, ps.length, ps.interned, ps.ascii, ps.compact, ps.ready) addr = id(s) + PyUnicodeObject.utf8_length.offset buf = (ctypes.c_char * 2).from_address(addr) print(addr, bytes(buf)) import pandas print(sys.getsizeof(s)) s = 'a' ps = PyUnicodeObject.from_address(s) print(ps, ps.kind, ps.length, ps.interned, ps.ascii, ps.compact, ps.ready) addr = id(s) + PyUnicodeObject.utf8_length.offset buf = (ctypes.c_char * 2).from_address(addr) print(addr, bytes(buf)) ,长度为1,凡人实习,ASCII,紧凑且准备就绪。

但是,如果您查看<__main__.PyUnicodeObject object at 0x101bbae18> 1 1 1 1 1 1 ,在Pandas之前是空指针,而在Pandas之后是指向1BYTE_KIND字符串ps.wstr的指针。并且wchar_t考虑到了w"a\0"的大小。


所以,问题是,如何最终得到一个具有str.__sizeof__值的ascii-compact字符串?

简单:您可以在其上调用PyUnicode_AsUnicode(或其他不赞成使用的函数或宏之一,以访问3.2样式的本机wstr内部存储器。该内部存储器实际上在3.3中不存在+。因此,为了实现向后兼容性,可以通过动态创建该存储,将其粘贴在wstr成员上,然后调用适当的wchar_t *函数来对该存储进行解码来处理这些调用。正在处理一个紧凑的字符串,其类型恰好匹配wstr的宽度,在这种情况下,PyUnicode_AsUCS[24]毕竟只是指向本机存储的指针。)

理想情况下,wchar_t应该包括该额外的存储空间,而from the source可以看到它确实存在。

让我们验证一下:

wstr

多田,我们的50分达到58分。


那么,您如何确定该调用的位置?

在整个Pandas和Numpy中,实际上有大量对str.__sizeof__import ctypes import sys s = 'a' print(sys.getsizeof(s)) ctypes.pythonapi.PyUnicode_AsUnicode.argtypes = [ctypes.py_object] ctypes.pythonapi.PyUnicode_AsUnicode.restype = ctypes.c_wchar_p print(ctypes.pythonapi.PyUnicode_AsUnicode(s)) print(sys.getsizeof(s)) 宏的调用以及其他调用它们的函数。因此,我在lldb中运行Python,并将断点附加到PyUnicode_AsUnicode上,并使用脚本跳过了调用堆栈帧是否与上次相同的情况。

前几个调用涉及日期时间格式。然后是一个带有单个字母的字母。堆栈框架是:

PyUnicode_AS_UNICODE

…以及PyUnicode_AsUnicode以上,直到multiarray.cpython-36m-darwin.so`UNICODE_setitem + 296 都是纯Python。因此,如果您想确切地了解Pandas在哪里调用此函数,则需要在multiarray中进行调试,而我尚未完成调试。但是我认为我们现在有足够的信息。

答案 2 :(得分:1)

这似乎与IPython启动文件中的单个Pandas导入有关。

我也可以在普通的Python会话中重现该行为:

 ~$ python
Python 3.6.6 |Anaconda, Inc.| (default, Jun 28 2018, 11:07:29) 
[GCC 4.2.1 Compatible Clang 4.0.1 (tags/RELEASE_401/final)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> from string import ascii_lowercase
>>> import sys
>>> strings = [ascii_lowercase[:i] for i, _ in enumerate(ascii_lowercase, 1)]
>>> sizes = dict(enumerate(map(sys.getsizeof, strings), 1))
>>> sizes
{1: 50, 2: 51, 3: 52, 4: 53, 5: 54, 6: 55, 7: 56, 8: 57, 9: 58, 10: 59, 11: 60, 12: 61, 13: 62, 14: 63, 15: 64, 16: 65, 17: 66, 18: 67, 19: 68, 20: 69, 21: 70, 22: 71, 23: 72, 24: 73, 25: 74, 26: 75}
>>> import pandas as pd
>>> sizes = dict(enumerate(map(sys.getsizeof, strings), 1))
>>> sizes
{1: 58, 2: 51, 3: 52, 4: 53, 5: 54, 6: 55, 7: 56, 8: 57, 9: 58, 10: 59, 11: 60, 12: 61, 13: 62, 14: 63, 15: 64, 16: 65, 17: 66, 18: 67, 19: 68, 20: 69, 21: 70, 22: 71, 23: 72, 24: 73, 25: 74, 26: 75}
>>> pd.__version__
'0.23.2'