每种编程语言为了表现出色,深入并且实现卓越的解释性能,都需要有大量编译器级与解释器级的器源优化。 由于字符串是码终明白任何编程语言中不可或缺的一个部分,因此,于搞原理如果有快速操作字符串的字符能力,就可以迅速地提高整体的串驻性能。 在本文中,深入我们将深入研究 Python 的解释内部实现,并了解 Python 如何使用一种名为字符串驻留(String Interning)的器源技术,实现解释器的码终明白高性能。 本文的于搞原理目的不仅在于介绍 Python 的内部知识,而且还旨在使读者能够轻松地浏览 Python 的字符源代码;因此,本文中将有很多出自 CPython 的串驻代码片段。 全文提纲如下: (在 Python猫 公众号回复数字“0215”,深入下载思维导图) 1、什么是“字符串驻留”? 字符串驻留是一种编译器/解释器的优化方法,高防服务器它通过缓存一般性的字符串,从而节省字符串处理任务的空间和时间。 这种优化方法不会每次都创建一个新的字符串副本,而是仅为每个适当的不可变值保留一个字符串副本,并使用指针引用之。 每个字符串的唯一拷贝被称为它的intern,并因此而得名 String Interning。 Python猫注:String Interning 一般被译为“字符串驻留”或“字符串留用”,在某些语言中可能习惯用 String Pool(字符串常量池)的概念,其实是对同一种机制的不同表述。intern 作为名词时,是“实习生、实习医生”的意思,在此可以理解成“驻留物、驻留值”。 查找字符串 intern 的方法可能作为公开接口公开,也可能不公开。现代编程语言如 Java、Python、PHP、香港云服务器Ruby、Julia 等等,都支持字符串驻留,以使其编译器和解释器做到高性能。 2、为什么要驻留字符串? 字符串驻留提升了字符串比较的速度。 如果没有驻留,当我们要比较两个字符串是否相等时,它的时间复杂度将上升到 O(n),即需要检查两个字符串中的每个字符,才能判断出它们是否相等。 但是,如果字符串是固定的,由于相同的字符串将使用同一个对象引用,因此只需检查指针是否相同,就足以判断出两个字符串是否相等,不必再逐一检查每个字符。由于这是一个非常普遍的操作,因此,它被典型地实现为指针相等性校验,云服务器提供商仅使用一条完全没有内存引用的机器指令。 字符串驻留减少了内存占用。 Python 避免内存中充斥多余的字符串对象,通过享元设计模式共享和重用已经定义的对象,从而优化内存占用。 3、Python的字符串驻留 像大多数其它现代编程语言一样,Python 也使用字符串驻留来提高性能。在 Python 中,我们可以使用is运算符,检查两个对象是否引用了同一个内存对象。 因此,如果两个字符串对象引用了相同的内存对象,则is运算符将得出True,否则为False。 我们可以使用这个特定的运算符,来判断哪些字符串是被驻留的。在 CPython 的,字符串驻留是通过以下函数实现的,声明在 unicodeobject.h 中,定义在 unicodeobject.c 中。 为了检查一个字符串是否被驻留,CPython 实现了一个名为PyUnicode_CHECK_INTERNED的宏,同样是定义在 unicodeobject.h 中。 这个宏表明了 Python 在PyASCIIObject结构中维护着一个名为interned的成员变量,它的值表示相应的字符串是否被驻留。 4、字符串驻留的原理 在 CPython 中,字符串的引用被一个名为interned的 Python 字典所存储、访问和管理。 该字典在第一次调用字符串驻留时,被延迟地初始化,并持有全部已驻留字符串对象的引用。 4.1 如何驻留字符串? 负责驻留字符串的核心函数是PyUnicode_InternInPlace,它定义在 unicodeobject.c 中,当调用时,它会创建一个准备容纳所有驻留的字符串的字典interned,然后登记入参中的对象,令其键和值都使用相同的对象引用。 以下函数片段显示了 Python 实现字符串驻留的过程。 4.2 如何清理驻留的字符串? 清理函数从interned字典中遍历所有的字符串,调整这些对象的引用计数,并把它们标记为NOT_INTERNED,使其被垃圾回收。一旦所有的字符串都被标记为NOT_INTERNED,则interned字典会被清空并删除。 这个清理函数就是_PyUnicode_ClearInterned,在 unicodeobject.c 中定义。 5、字符串驻留的实现 既然了解了字符串驻留及清理的内部原理,我们就可以找出 Python 中所有会被驻留的字符串。 为了做到这点,我们要做的就是在 CPython 源代码中查找PyUnicode_InternInPlace 函数的调用,并查看其附近的代码。下面是在 Python 中关于字符串驻留的一些有趣的发现。 5.1 变量、常量与函数名 CPython 对常量(例如函数名、变量名、字符串字面量等)执行字符串驻留。 以下代码出自codeobject.c,它表明在创建新的PyCode对象时,解释器将对所有编译期的常量、名称和字面量进行驻留。 5.2 字典的键 CPython 还会驻留任何字典对象的字符串键。 当在字典中插入元素时,解释器会对该元素的键作字符串驻留。以下代码出自 dictobject.c,展示了实际的行为。 有趣的地方:在PyUnicode_InternInPlace函数被调用处有一条注释,它问道,我们是否真的需要对所有字典中的全部键进行驻留? 5.3 任何对象的属性 Python 中对象的属性可以通过setattr函数显式地设置,也可以作为类成员的一部分而隐式地设置,或者在其数据类型中预定义。 CPython 会驻留所有这些属性名,以便实现快速查找。 以下是函数PyObject_SetAttr的代码片段,该函数定义在文件object.c中,负责为 Python 对象设置新属性。 5.4 显式地驻留 Python 还支持通过sys模块中的intern函数进行显式地字符串驻留。 当使用任何字符串对象调用此函数时,该字符串对象将被驻留。以下是 sysmodule.c 文件的代码片段,它展示了在sys_intern_impl函数中的字符串驻留过程。 6、字符串驻留的其它发现 只有编译期的字符串会被驻留。 在解释时或编译时指定的字符串会被驻留,而动态创建的字符串则不会。 Python猫注:这一条规则值得展开思考,我曾经在上面踩过坑……有两个知识点,我相信 99% 的人都不知道:字符串的 join() 方法是动态创建字符串,因此其创建的字符串不会被驻留;常量折叠机制也发生在编译期,因此有时候容易把它跟字符串驻留搞混淆。 包含 ASCII 字符和下划线的字符串会被驻留。 在编译期间,当对字符串字面量进行驻留时,CPython 确保仅对匹配正则表达式[a-zA-Z0-9_]*的常量进行驻留,因为它们非常贴近于 Python 的标识符。 Python猫注:关于 Python 中标识符的命名规则,在 Python2 版本只有“字母、数字和下划线”,但在 Python 3.x 版本中,已经支持 Unicode 编码。 参考材料字符串驻留(https://en.wikipedia.org/wiki/String_interning) CPython优化(https://stummjr.org/post/cpython-optimizations/) Python对象第三部分:字符串驻留(https://medium.com/@bdov_/https-medium-com-bdov-python-objects-part-iii-string-interning-625d3c7319de) Python字符串驻留的内部原理(http://guilload.com/python-string-interning/) Python优化机制:常量折叠(https://mp.weixin.qq.com/s/p1Zb_linFLWwPlNyA5Ui1Q) join()方法的神奇用处与Intern机制的软肋(https://mp.weixin.qq.com/s/M2uHVqaHe_nyO5jT60V_6Q) 英文:https://arpitbhayani.me/blogs/string-interning 作者:arpit 译者:豌豆花下猫(“Python猫”公众号作者) 声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。 本文转载自微信公众号「Python猫」,可以通过以下二维码关注。转载本文请联系Python猫公众号。