通过查看CPython
实现,似乎字符串split()
的返回值是新分配的字符串的列表。但是,由于字符串是不可变的,因此似乎可以通过指向偏移量来从原始字符串中创建子字符串。
我是否正确理解了CPython的当前行为?有理由不选择这种空间优化吗?我能想到的一个原因是,在父字符串的所有子字符串都释放之前,无法释放父字符串。
没有水晶球,我无法告诉你为什么CPython会这样做。 但是,您可能会选择这样做的原因有一些。
问题是一个小字符串可能包含对更大的后备数组的引用。 例如,假设我读取 8 GB HTTP 访问日志文件以分析哪些用户代理访问我的文件最多,我只需通过fp.read()
来执行此操作,然后一次对整个文件运行正则表达式,而不是一次运行一行。
我想知道前 10 个最常见的用户代理,所以我把它放在一个列表中。
然后,我想对其他 100 个文件进行相同的分析,以查看前 10 个用户代理随时间的变化情况。 繁荣! 我的程序正在尝试使用 800 GB 内存并被杀死。 为什么? 如何调试?
Java在Java 7之前使用了这种共享技术,所以同样的理由也适用。 请参阅 Java 7 字符串 - 子字符串复杂性和 JDK-4513622:(str) 保留字段的子字符串可防止对象的 GC。
另请注意,让字符串共享内存需要您跟随从字符串对象到字符串数据的指针。 在CPython中,字符串数据通常直接放在内存中的标头之后,因此您无需遵循指针。 这减少了所需的分配数量,并减少了读取字符串时的数据依赖性。
在当前的CPython实现中,字符串是引用计数的;假定字符串不能保存对其他对象的引用,因为字符串不是容器。 这意味着垃圾回收不需要检查或跟踪字符串对象(因为它们完全被引用计数覆盖)。 但实际上比这更糟糕的是:旧版本的 Python 根本没有跟踪垃圾收集器;GC 是 2.0 中的新功能。 在此之前,任何循环垃圾都会泄漏。
有效实现的子字符串到偏移量算法不应形成循环。 所以从理论上讲,循环垃圾收集器不是先决条件。 但是,由于我们执行的是引用计数而不是跟踪,因此子对象负责在生命周期结束时Py_DECREF()
其父对象。 否则父级泄漏。 这意味着您不能在生命周期结束时将整个字符串放入空闲列表中;您必须检查它是否是子字符串,并且分支可能很昂贵。 Python历史上被设计为进行字符串处理(如Perl,但语法更好),这意味着创建和销毁大量字符串。 此外,所有变量名在内部存储为字符串,因此即使用户不进行字符串处理,解释器也会进行字符串处理。 将字符串释放过程减慢一点点,都可能对性能产生严重影响。
CPython除了存储长度外,还在内部使用以NUL结尾的字符串。这是一个非常早期的设计选择,从Python的第一个版本就存在,并且在最新版本中仍然如此。
您可以在 Include/unicodeobject.h 中看到,其中 PyASCIIObject 表示"wchar_t表示(null 终止)",PyCompactUnicodeObject 表示"UTF-8 表示(null 终止)"。(最近的 CPython 实现从 4 种后端字符串类型中选择一种,具体取决于 Unicode 编码需求。
许多 Python 扩展模块需要 NUL 终止的字符串。很难将子字符串作为切片实现为更大的字符串并保留低级 C API。并非不可能,因为它可以使用 copy-on-C-API 访问来完成。或者 Python 可以要求所有扩展编写者使用新的子切片友好 API。但是,正如Dietrich Epp所描述的那样,考虑到从其他实现子切片引用的语言中发现的问题,这种复杂性是不值得的。
我在凯文的回答中看不出适用于这个问题。这个决定与Python 2.0之前缺乏循环垃圾收集无关,也不能。子字符串切片是使用非循环数据结构实现的。"胜任实施"不是一个相关的要求,因为它需要一种反常的无能或恶意才能将其转化为循环数据结构。
释放分配器中也不一定有额外的分支开销。如果源字符串是一种类型,而子字符串切片另一种类型,则 Python 的普通类型调度程序将自动使用正确的分配器,而不会产生额外的开销。即使有一个额外的分支,我们知道在这种情况下分支开销并不"昂贵"。Python 3.3(因为 PEP 393)具有这 4 种后端 Unicode 类型,并根据分支决定做什么。字符串访问比释放更频繁地发生,因此由于分支而导致的任何分置开销都将在噪音中丢失。
在CPython中,"变量名称在内部存储为字符串",这是最正确的。(例外情况是局部变量作为索引存储到局部数组中。但是,这些名称也使用 PyUnicode_InternInPlace() 驻留在全局字典中。因此,没有释放开销,因为这些字符串没有被解除分配,除了涉及使用非驻留字符串的动态调度的情况,例如通过 getattr()。