在Python 3.12中,为什么'Öl'比'Ö'占用更少的内存?

问题描述 投票:0回答:1

我刚刚阅读了PEP 393并了解到Python的

str
类型根据内容使用不同的内部表示。所以,我做了一些实验,结果让我有点惊讶:

>>> sys.getsizeof('')
41
>>> sys.getsizeof('H')
42
>>> sys.getsizeof('Hi')
43
>>> sys.getsizeof('Ö')
61
>>> sys.getsizeof('Öl')
59

我知道在前三种情况下,字符串不包含任何非 ASCII 字符,因此可以使用每个字符 1 个字节的编码。将

Ö
等非 ASCII 字符放入字符串中会强制解释器使用不同的编码。因此,我对
'Ö'
'H'
占用更多空间并不感到惊讶。

但是,为什么

'Öl'
'Ö'
占用的空间更少?我假设无论用于
'Öl'
的内部表示都允许更短的
'Ö'
表示。

我使用的是Python 3.12,显然它在早期版本中无法重现。

python string python-internals python-3.12
1个回答
0
投票

此测试代码(结构仅根据 3.12.4 源正确,即使如此我也没有仔细检查它们)

import ctypes
import sys


class PyUnicodeObject(ctypes.Structure):
    _fields_ = [
        ("ob_refcnt", ctypes.c_ssize_t),
        ("ob_type", ctypes.c_void_p),
        ("length", ctypes.c_ssize_t),
        ("hash", ctypes.c_ssize_t),
        ("state", ctypes.c_uint64),
    ]


class StateBitField(ctypes.LittleEndianStructure):
    _fields_ = [
        ("interned", ctypes.c_uint, 2),
        ("kind", ctypes.c_uint, 3),
        ("compact", ctypes.c_uint, 1),
        ("ascii", ctypes.c_uint, 1),
        ("statically_allocated", ctypes.c_uint, 1),
        ("_padding", ctypes.c_uint, 24),
    ]

    def __repr__(self):
        return ", ".join(f"{k}: {getattr(self, k)}" for k, *_ in self._fields_)


def dump_s(s: str):
    o = PyUnicodeObject.from_address(id(s))
    print("===")
    print(f"{s=!r}, {o.length=}, {sys.getsizeof(s)=}")
    state_int = o.state
    state = StateBitField.from_buffer(ctypes.c_uint64(state_int))
    print(state)


dump_s(str(chr(214)))
dump_s(str(chr(214) + chr(108)))

打印出来

===
s='Ö', o.length=1, sys.getsizeof(s)=61
interned: 0, kind: 1, compact: 1, ascii: 0, statically_allocated: 1, _padding: 0
===
s='Öl', o.length=2, sys.getsizeof(s)=59
interned: 0, kind: 1, compact: 1, ascii: 0, statically_allocated: 0, _padding: 43023

– 确凿无疑的证据似乎是

statically_allocated
Ö

我认为这源于 pycore_runtime_init_generated

 中的这一行,它看起来像是所有 Latin-1 字符串(以及其他字符串)的运行时静态对象。

虽然可能是错的..!

© www.soinside.com 2019 - 2024. All rights reserved.