立即注册 找回密码

QQ登录

只需一步,快速开始

查看: 1135|回复: 0

[Discuz 通用教程] 深入 Python 解释器源码,我终于搞明白了字符串驻留的原理

[复制链接]
发表于 2023-2-22 10:37:33 | 显示全部楼层 |阅读模式
道勤网-数据www.daoqin.net

亲注册登录道勤网-可以查看更多帖子内容哦!(包涵精彩图片、文字详情等)请您及时注册登录-www.daoqin.net

您需要 登录 才可以下载或查看,没有账号?立即注册

x
深入 Python 解释器源码,我终于搞明白了字符串驻留的原理

声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。

每种编程语言为了表现出色,并且实现卓越的性能,都需要有大量编译器级与解释器级的优化。

由于字符串是任何编程语言中不可或缺的一个部分,因此,如果有快速操作字符串的能力,就可以迅速地提高整体的性能。

在本文中,我们将深入研究 Python 的内部实现,并了解 Python 如何使用一种名为字符串驻留(String Interning)的技术,实现解释器的高性能。 本文的目的不仅在于介绍 Python 的内部知识,而且还旨在使读者能够轻松地浏览 Python 的源代码;因此,本文中将有很多出自 CPython 的代码片段。

全文提纲如下:

01.jpg

1、什么是 “字符串驻留”?

字符串驻留是一种编译器 / 解释器的优化方法,它通过缓存一般性的字符串,从而节省字符串处理任务的空间和时间。

这种优化方法不会每次都创建一个新的字符串副本,而是仅为每个适当的不可变值保留一个字符串副本,并使用指针引用之。

每个字符串的唯一拷贝被称为它的 intern,并因此而得名 String Interning。

Python 猫注:String Interning 一般被译为 “字符串驻留” 或 “字符串留用”,在某些语言中可能习惯用 String Pool(字符串常量池)的概念,其实是对同一种机制的不同表述。intern 作为名词时,是 “实习生、实习医生” 的意思,在此可以理解成 “驻留物、驻留值”。

查找字符串 intern 的方法可能作为公开接口公开,也可能不公开。现代编程语言如 Java、Python、PHP、Ruby、Julia 等等,都支持字符串驻留,以使其编译器和解释器做到高性能。

02.jpg

2、为什么要驻留字符串?

字符串驻留提升了字符串比较的速度。 如果没有驻留,当我们要比较两个字符串是否相等时,它的时间复杂度将上升到 O (n),即需要检查两个字符串中的每个字符,才能判断出它们是否相等。

但是,如果字符串是固定的,由于相同的字符串将使用同一个对象引用,因此只需检查指针是否相同,就足以判断出两个字符串是否相等,不必再逐一检查每个字符。由于这是一个非常普遍的操作,因此,它被典型地实现为指针相等性校验,仅使用一条完全没有内存引用的机器指令。

字符串驻留减少了内存占用。 Python 避免内存中充斥多余的字符串对象,通过享元设计模式共享和重用已经定义的对象,从而优化内存占用。

3、Python 的字符串驻留

像大多数其它现代编程语言一样,Python 也使用字符串驻留来提高性能。在 Python 中,我们可以使用 is 运算符,检查两个对象是否引用了同一个内存对象。

因此,如果两个字符串对象引用了相同的内存对象,则 is 运算符将得出 True,否则为 False。

  1. >>> 'python' is 'python'
  2. True
复制代码

我们可以使用这个特定的运算符,来判断哪些字符串是被驻留的。在 CPython 的,字符串驻留是通过以下函数实现的,声明在 unicodeobject.h 中,定义在 unicodeobject.c 中。

  1. PyAPI_FUNC(void) PyUnicode_InternInPlace(PyObject **);
复制代码

为了检查一个字符串是否被驻留,CPython 实现了一个名为 PyUnicode_CHECK_INTERNED 的宏,同样是定义在 unicodeobject.h 中。

这个宏表明了 Python 在 PyASCIIObject 结构中维护着一个名为 interned 的成员变量,它的值表示相应的字符串是否被驻留。

  1. #define PyUnicode_CHECK_INTERNED(op) \
  2.     (((PyASCIIObject *)(op))->state.interned)
复制代码
4、字符串驻留的原理

在 CPython 中,字符串的引用被一个名为 interned 的 Python 字典所存储、访问和管理。 该字典在第一次调用字符串驻留时,被延迟地初始化,并持有全部已驻留字符串对象的引用。

4.1 如何驻留字符串?

负责驻留字符串的核心函数是 PyUnicode_InternInPlace,它定义在 unicodeobject.c 中,当调用时,它会创建一个准备容纳所有驻留的字符串的字典 interned,然后登记入参中的对象,令其键和值都使用相同的对象引用。

以下函数片段显示了 Python 实现字符串驻留的过程。

  1. void
  2. PyUnicode_InternInPlace(PyObject **p)
  3. {
  4.     PyObject *s = *p;

  5.     .........

  6.     // Lazily build the dictionary to hold interned Strings
  7.     if (interned == NULL) {
  8.         interned = PyDict_New();
  9.         if (interned == NULL) {
  10.             PyErr_Clear();
  11.             return;
  12.         }
  13.     }

  14.     PyObject *t;

  15.     // Make an entry to the interned dictionary for the
  16.     // given object
  17.     t = PyDict_SetDefault(interned, s, s);

  18.     .........
  19.    
  20.     // The two references in interned dict (key and value) are
  21.     // not counted by refcnt.
  22.     // unicode_dealloc() and _PyUnicode_ClearInterned() take
  23.     // care of this.
  24.     Py_SET_REFCNT(s, Py_REFCNT(s) - 2);

  25.     // Set the state of the string to be INTERNED
  26.     _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
  27. }
复制代码
4.2 如何清理驻留的字符串?

清理函数从 interned 字典中遍历所有的字符串,调整这些对象的引用计数,并把它们标记为 NOT_INTERNED,使其被垃圾回收。一旦所有的字符串都被标记为 NOT_INTERNED,则 interned 字典会被清空并删除。

这个清理函数就是_PyUnicode_ClearInterned,在 unicodeobject.c 中定义。

  1. void
  2. _PyUnicode_ClearInterned(PyThreadState *tstate)
  3. {
  4.     .........

  5.     // Get all the keys to the interned dictionary
  6.     PyObject *keys = PyDict_Keys(interned);

  7.     .........

  8.     // Interned Unicode strings are not forcibly deallocated;
  9.     // rather, we give them their stolen references back
  10.     // and then clear and DECREF the interned dict.

  11.     for (Py_ssize_t i = 0; i < n; i++) {
  12.         PyObject *s = PyList_GET_ITEM(keys, i);

  13.         .........

  14.         switch (PyUnicode_CHECK_INTERNED(s)) {
  15.         case SSTATE_INTERNED_IMMORTAL:
  16.             Py_SET_REFCNT(s, Py_REFCNT(s) + 1);
  17.             break;
  18.         case SSTATE_INTERNED_MORTAL:
  19.             // Restore the two references (key and value) ignored
  20.             // by PyUnicode_InternInPlace().
  21.             Py_SET_REFCNT(s, Py_REFCNT(s) + 2);
  22.             break;
  23.         case SSTATE_NOT_INTERNED:
  24.             /* fall through */
  25.         default:
  26.             Py_UNREACHABLE();
  27.         }

  28.         // marking the string to be NOT_INTERNED
  29.         _PyUnicode_STATE(s).interned = SSTATE_NOT_INTERNED;
  30.     }

  31.     // decreasing the reference to the initialized and
  32.     // access keys object.
  33.     Py_DECREF(keys);

  34.     // clearing the dictionary
  35.     PyDict_Clear(interned);

  36.     // clearing the object interned
  37.     Py_CLEAR(interned);
  38. }
复制代码
5、字符串驻留的实现

既然了解了字符串驻留及清理的内部原理,我们就可以找出 Python 中所有会被驻留的字符串。

为了做到这点,我们要做的就是在 CPython 源代码中查找 PyUnicode_InternInPlace 函数的调用,并查看其附近的代码。下面是在 Python 中关于字符串驻留的一些有趣的发现。

5.1 变量、常量与函数名

CPython 对常量(例如函数名、变量名、字符串字面量等)执行字符串驻留。

以下代码出自 codeobject.c,它表明在创建新的 PyCode 对象时,解释器将对所有编译期的常量、名称和字面量进行驻留。

  1. PyCodeObject *
  2. PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount,
  3.                           int nlocals, int stacksize, int flags,
  4.                           PyObject *code, PyObject *consts, PyObject *names,
  5.                           PyObject *varnames, PyObject *freevars, PyObject *cellvars,
  6.                           PyObject *filename, PyObject *name, int firstlineno,
  7.                           PyObject *linetable)
  8. {

  9.     ........

  10.     if (intern_strings(names) < 0) {
  11.         return NULL;
  12.     }

  13.     if (intern_strings(varnames) < 0) {
  14.         return NULL;
  15.     }

  16.     if (intern_strings(freevars) < 0) {
  17.         return NULL;
  18.     }

  19.     if (intern_strings(cellvars) < 0) {
  20.         return NULL;
  21.     }

  22.     if (intern_string_constants(consts, NULL) < 0) {
  23.         return NULL;
  24.     }

  25.     ........

  26. }
复制代码
5.2 字典的键

CPython 还会驻留任何字典对象的字符串键。

当在字典中插入元素时,解释器会对该元素的键作字符串驻留。以下代码出自 dictobject.c,展示了实际的行为。

有趣的地方:在 PyUnicode_InternInPlace 函数被调用处有一条注释,它问道,我们是否真的需要对所有字典中的全部键进行驻留?

  1. int
  2. PyDict_SetItemString(PyObject *v, const char *key, PyObject *item)
  3. {
  4.     PyObject *kv;
  5.     int err;
  6.     kv = PyUnicode_FromString(key);
  7.     if (kv == NULL)
  8.         return -1;

  9.     // Invoking String Interning on the key
  10.     PyUnicode_InternInPlace(&kv); /* XXX Should we really? */

  11.     err = PyDict_SetItem(v, kv, item);
  12.     Py_DECREF(kv);
  13.     return err;
  14. }
复制代码
5.3 任何对象的属性

Python 中对象的属性可以通过 setattr 函数显式地设置,也可以作为类成员的一部分而隐式地设置,或者在其数据类型中预定义。

CPython 会驻留所有这些属性名,以便实现快速查找。 以下是函数 PyObject_SetAttr 的代码片段,该函数定义在文件 object.c 中,负责为 Python 对象设置新属性。

  1. int
  2. PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value)
  3. {

  4.     ........

  5.     PyUnicode_InternInPlace(&name);

  6.     ........
  7. }
复制代码
5.4 显式地驻留

Python 还支持通过 sys 模块中的 intern 函数进行显式地字符串驻留。

当使用任何字符串对象调用此函数时,该字符串对象将被驻留。以下是 sysmodule.c 文件的代码片段,它展示了在 sys_intern_impl 函数中的字符串驻留过程。

  1. static PyObject *
  2. sys_intern_impl(PyObject *module, PyObject *s)
  3. {

  4.     ........

  5.     if (PyUnicode_CheckExact(s)) {
  6.         Py_INCREF(s);
  7.         PyUnicode_InternInPlace(&s);
  8.         return s;
  9.     }

  10.     ........
  11. }
复制代码
6、字符串驻留的其它发现

只有编译期的字符串会被驻留。 在解释时或编译时指定的字符串会被驻留,而动态创建的字符串则不会。

Python 猫注:这一条规则值得展开思考,我曾经在上面踩过坑…… 有两个知识点,我相信 99% 的人都不知道:字符串的 join () 方法是动态创建字符串,因此其创建的字符串不会被驻留;常量折叠机制也发生在编译期,因此有时候容易把它跟字符串驻留搞混淆。推荐阅读《join () 方法的神奇用处与 Intern 机制的软肋

包含 ASCII 字符和下划线的字符串会被驻留。 在编译期间,当对字符串字面量进行驻留时,CPython 确保仅对匹配正则表达式 [a-zA-Z0-9_]* 的常量进行驻留,因为它们非常贴近于 Python 的标识符。

Python 猫注:关于 Python 中标识符的命名规则,在 Python2 版本只有 “字母、数字和下划线”,但在 Python 3.x 版本中,已经支持 Unicode 编码。这部分内容推荐阅读《醒醒!Python 已经支持中文变量名啦!

参考材料


道勤主机提供365天*24小时全年全天无休、实时在线、零等待的售后技术支持。竭力为您免费处理您在使用道勤主机过程中所遇到的一切问题! 如果您是道勤主机用户,那么您可以通过QQ【792472177】、售后QQ【59133755】、旺旺【诠释意念】、微信:q792472177免费电话、后台提交工单这些方式联系道勤主机客服! 如果您不是我们的客户也没问题,点击页面最右边的企业QQ在线咨询图标联系我们并购买后,我们为您免费进行无缝搬家服务,让您享受网站零访问延迟的迁移到道勤主机的服务!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关闭

道勤网- 推荐内容!上一条 /2 下一条

!jz_fbzt! !jz_sgzt! !jz_xgzt! 快速回复 !jz_fhlb! !jz_lxwm! !jz_gfqqq!

关于我们|手机版|小黑屋|地图|【道勤网】-www.daoqin.net 软件视频自学教程|免费教程|自学电脑|3D教程|平面教程|影视动画教程|办公教程|机械设计教程|网站设计教程【道勤网】 ( 皖ICP备15000319号-1 )

GMT+8, 2024-4-26 09:27

Powered by DaoQin! X3.4 © 2016-2063 Dao Qin & 道勤科技

快速回复 返回顶部 返回列表