深入 Python 解释器源码,我终于搞明白了字符串驻留的原理!
英文:https://arpitbhayani.me/blogs/string-interning
作者:arpit
译者:豌豆花下猫(“Python猫”公众号作者)
声明:本翻译是出于交流学习的目的,基于 CC BY-NC-SA 4.0 授权协议。为便于阅读,内容略有改动。
每种编程语言为了表现出色,并且实现卓越的性能,都需要有大量编译器级与解释器级的优化。
由于字符串是任何编程语言中不可或缺的一个部分,因此,如果有快速操作字符串的能力,就可以迅速地提高整体的性能。
在本文中,我们将深入研究 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
。
>>> 'python' is 'python'
True
我们可以使用这个特定的运算符,来判断哪些字符串是被驻留的。在 CPython 的,字符串驻留是通过以下函数实现的,声明在 unicodeobject.h 中,定义在 unicodeobject.c 中。
PyAPI_FUNC(void) PyUnicode_InternInPlace(PyObject **);
为了检查一个字符串是否被驻留,CPython 实现了一个名为PyUnicode_CHECK_INTERNED
的宏,同样是定义在 unicodeobject.h 中。
这个宏表明了 Python 在PyASCIIObject
结构中维护着一个名为interned
的成员变量,它的值表示相应的字符串是否被驻留。
#define PyUnicode_CHECK_INTERNED(op) \
(((PyASCIIObject *)(op))->state.interned)
4、字符串驻留的原理
在 CPython 中,字符串的引用被一个名为interned
的 Python 字典所存储、访问和管理。 该字典在第一次调用字符串驻留时,被延迟地初始化,并持有全部已驻留字符串对象的引用。
4.1 如何驻留字符串?
负责驻留字符串的核心函数是PyUnicode_InternInPlace
,它定义在 unicodeobject.c 中,当调用时,它会创建一个准备容纳所有驻留的字符串的字典interned
,然后登记入参中的对象,令其键和值都使用相同的对象引用。
以下函数片段显示了 Python 实现字符串驻留的过程。
void
PyUnicode_InternInPlace(PyObject **p)
{
PyObject *s = *p;
.........
// Lazily build the dictionary to hold interned Strings
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear();
return;
}
}
PyObject *t;
// Make an entry to the interned dictionary for the
// given object
t = PyDict_SetDefault(interned, s, s);
.........
// The two references in interned dict (key and value) are
// not counted by refcnt.
// unicode_dealloc() and _PyUnicode_ClearInterned() take
// care of this.
Py_SET_REFCNT(s, Py_REFCNT(s) - 2);
// Set the state of the string to be INTERNED
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}
4.2 如何清理驻留的字符串?
清理函数从interned
字典中遍历所有的字符串,调整这些对象的引用计数,并把它们标记为NOT_INTERNED
,使其被垃圾回收。一旦所有的字符串都被标记为NOT_INTERNED
,则interned
字典会被清空并删除。
这个清理函数就是_PyUnicode_ClearInterned
,在unicodeobject.c 中定义。
void
_PyUnicode_ClearInterned(PyThreadState *tstate)
{
.........
// Get all the keys to the interned dictionary
PyObject *keys = PyDict_Keys(interned);
.........
// Interned Unicode strings are not forcibly deallocated;
// rather, we give them their stolen references back
// and then clear and DECREF the interned dict.
for (Py_ssize_t i = 0; i < n; i++) {
PyObject *s = PyList_GET_ITEM(keys, i);
.........
switch (PyUnicode_CHECK_INTERNED(s)) {
case SSTATE_INTERNED_IMMORTAL:
Py_SET_REFCNT(s, Py_REFCNT(s) + 1);
break;
case SSTATE_INTERNED_MORTAL:
// Restore the two references (key and value) ignored
// by PyUnicode_InternInPlace().
Py_SET_REFCNT(s, Py_REFCNT(s) + 2);
break;
case SSTATE_NOT_INTERNED:
/* fall through */
default:
Py_UNREACHABLE();
}
// marking the string to be NOT_INTERNED
_PyUnicode_STATE(s).interned = SSTATE_NOT_INTERNED;
}
// decreasing the reference to the initialized and
// access keys object.
Py_DECREF(keys);
// clearing the dictionary
PyDict_Clear(interned);
// clearing the object interned
Py_CLEAR(interned);
}
5、字符串驻留的实现
既然了解了字符串驻留及清理的内部原理,我们就可以找出 Python 中所有会被驻留的字符串。
为了做到这点,我们要做的就是在 CPython 源代码中查找PyUnicode_InternInPlace
函数的调用,并查看其附近的代码。下面是在 Python 中关于字符串驻留的一些有趣的发现。
5.1 变量、常量与函数名
CPython 对常量(例如函数名、变量名、字符串字面量等)执行字符串驻留。
以下代码出自codeobject.c,它表明在创建新的PyCode
对象时,解释器将对所有编译期的常量、名称和字面量进行驻留。
PyCodeObject *
PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount,
int nlocals, int stacksize, int flags,
PyObject *code, PyObject *consts, PyObject *names,
PyObject *varnames, PyObject *freevars, PyObject *cellvars,
PyObject *filename, PyObject *name, int firstlineno,
PyObject *linetable)
{
........
if (intern_strings(names) < 0) {
return NULL;
}
if (intern_strings(varnames) < 0) {
return NULL;
}
if (intern_strings(freevars) < 0) {
return NULL;
}
if (intern_strings(cellvars) < 0) {
return NULL;
}
if (intern_string_constants(consts, NULL) < 0) {
return NULL;
}
........
}
5.2 字典的键
CPython 还会驻留任何字典对象的字符串键。
当在字典中插入元素时,解释器会对该元素的键作字符串驻留。以下代码出自 dictobject.c,展示了实际的行为。
有趣的地方:在PyUnicode_InternInPlace
函数被调用处有一条注释,它问道,我们是否真的需要对所有字典中的全部键进行驻留?
int
PyDict_SetItemString(PyObject *v, const char *key, PyObject *item)
{
PyObject *kv;
int err;
kv = PyUnicode_FromString(key);
if (kv == NULL)
return -1;
// Invoking String Interning on the key
PyUnicode_InternInPlace(&kv); /* XXX Should we really? */
err = PyDict_SetItem(v, kv, item);
Py_DECREF(kv);
return err;
}
5.3 任何对象的属性
Python 中对象的属性可以通过setattr
函数显式地设置,也可以作为类成员的一部分而隐式地设置,或者在其数据类型中预定义。
CPython 会驻留所有这些属性名,以便实现快速查找。 以下是函数PyObject_SetAttr
的代码片段,该函数定义在文件object.c中,负责为 Python 对象设置新属性。
int
PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value)
{
........
PyUnicode_InternInPlace(&name);
........
}
5.4 显式地驻留
Python 还支持通过sys
模块中的intern
函数进行显式地字符串驻留。
当使用任何字符串对象调用此函数时,该字符串对象将被驻留。以下是 sysmodule.c 文件的代码片段,它展示了在sys_intern_impl
函数中的字符串驻留过程。
static PyObject *
sys_intern_impl(PyObject *module, PyObject *s)
{
........
if (PyUnicode_CheckExact(s)) {
Py_INCREF(s);
PyUnicode_InternInPlace(&s);
return s;
}
........
}
6、字符串驻留的其它发现
只有编译期的字符串会被驻留。 在解释时或编译时指定的字符串会被驻留,而动态创建的字符串则不会。
Python猫注:这一条规则值得展开思考,我曾经在上面踩过坑……有两个知识点,我相信 99% 的人都不知道:字符串的 join() 方法是动态创建字符串,因此其创建的字符串不会被驻留;常量折叠机制也发生在编译期,因此有时候容易把它跟字符串驻留搞混淆。推荐阅读《join()方法的神奇用处与Intern机制的软肋》
包含 ASCII 字符和下划线的字符串会被驻留。 在编译期间,当对字符串字面量进行驻留时,CPython 确保仅对匹配正则表达式[a-zA-Z0-9_]*
的常量进行驻留,因为它们非常贴近于 Python 的标识符。
Python猫注:关于 Python 中标识符的命名规则,在 Python2 版本只有“字母、数字和下划线”,但在 Python 3.x 版本中,已经支持 Unicode 编码。这部分内容推荐阅读《醒醒!Python已经支持中文变量名啦!》
参考材料
- 字符串驻留(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)
深入 Python 解释器源码,我终于搞明白了字符串驻留的原理!的更多相关文章
- 《python解释器源码剖析》第9章--python虚拟机框架
9.0 序 下面我们就来剖析python运行字节码的原理,我们知道python虚拟机是python的核心,在源代码被编译成字节码序列之后,就将有python的虚拟机接手整个工作.python虚拟机会从 ...
- 《python解释器源码剖析》第0章--python的架构与编译python
本系列是以陈儒先生的<python源码剖析>为学习素材,所记录的学习内容.不同的是陈儒先生的<python源码剖析>所剖析的是python2.5,本系列对应的是python3. ...
- 《python解释器源码剖析》第13章--python虚拟机中的类机制
13.0 序 这一章我们就来看看python中类是怎么实现的,我们知道C不是一个面向对象语言,而python却是一个面向对象的语言,那么在python的底层,是如何使用C来支持python实现面向对象 ...
- 《python解释器源码剖析》第12章--python虚拟机中的函数机制
12.0 序 函数是任何一门编程语言都具备的基本元素,它可以将多个动作组合起来,一个函数代表了一系列的动作.当然在调用函数时,会干什么来着.对,要在运行时栈中创建栈帧,用于函数的执行. 在python ...
- 《python解释器源码剖析》第8章--python的字节码与pyc文件
8.0 序 我们日常会写各种各样的python脚本,在运行的时候只需要输入python xxx.py程序就执行了.那么问题就来了,一个py文件是如何被python变成一系列的机器指令并执行的呢? 8. ...
- 《python解释器源码剖析》第4章--python中的list对象
4.0 序 python中的list对象,底层对应的则是PyListObject.如果你熟悉C++,那么会很容易和C++中的list联系起来.但实际上,这个C++中的list大相径庭,反而和STL中的 ...
- 《python解释器源码剖析》第2章--python中的int对象
2.0 序 在所有的python内建对象中,整数对象是最简单的对象.从对python对象机制的剖析来看,整数对象是一个非常好的切入点.那么下面就开始剖析整数对象的实现机制 2.1 初识PyLongOb ...
- 《python解释器源码剖析》第1章--python对象初探
1.0 序 对象是python中最核心的一个概念,在python的世界中,一切都是对象,整数.字符串.甚至类型.整数类型.字符串类型,都是对象.换句话说,python中面向对象的理念观测的非常彻底,面 ...
- 《python解释器源码剖析》第11章--python虚拟机中的控制流
11.0 序 在上一章中,我们剖析了python虚拟机中的一般表达式的实现.在剖析一遍表达式是我们的流程都是从上往下顺序执行的,在执行的过程中没有任何变化.但是显然这是不够的,因为怎么能没有流程控制呢 ...
随机推荐
- Python数据模型与Python对象模型
数据模型==对象模型 Python官方文档说法是"Python数据模型",大多数Python书籍作者说法是"Python对象模型",它们是一个意思,表示&quo ...
- ovs-vsctl命令
ovs-vsctl [options] -- [options] command [args] [-- [options] command [args]]... 通过连接到 ovsdb-server ...
- 解决JavaScript中构造函数浪费内存的问题!
解决JavaScript中构造函数浪费内存的问题! 把构造函数中的公共的方法放到构造函数的原型对象上! // 构造函数的问题! function Gouzaohanshu(name, age, gen ...
- Vim中的swp文件,在vim非正常退出时,再次编辑会出问题
vim中的swp即swap文件,在编辑文件时产生,它是隐藏文件,如果原文件名是data,那么swp文件名就是.data.swp.如果文件正常退出,则此文件自动删除.以下两种情况不会删除swp文件: V ...
- Maven 本地仓库
概述 Maven 的本地资源库是用来存储所有项目的依赖关系(插件 Jar 和其他文件,这些文件被 Maven 下载)到本地文件夹.很简单,当你建立一个 Maven 项目,所有相关文件将被存储在你的 M ...
- Frame of Reference and Roaring Bitmaps
https://www.elastic.co/cn/blog/frame-of-reference-and-roaring-bitmaps http://roaringbitmap.org/ 2015 ...
- 对话 CTO〡用声音在一起,听荔枝 CTO 丁宁聊 UGC 声音互动平台的技术世界 原创 王颖奇 极客公园 2018-12-01
https://mp.weixin.qq.com/s/jfHFXZpzbAEbHKkCMSev6w 对话 CTO〡用声音在一起,听荔枝 CTO 丁宁聊 UGC 声音互动平台的技术世界 原创 王颖奇 极 ...
- 避免重复提交?分布式服务的幂等性设计! 架构文摘 今天 点击蓝色“架构文摘”关注我哟 加个“星标”,每天上午 09:25,干货推送! 来源:https://www.cnblogs.com/QG-whz/p/10372458.html 作者:melonstreet
避免重复提交?分布式服务的幂等性设计! 架构文摘 今天 点击蓝色"架构文摘"关注我哟 加个"星标",每天上午 09:25,干货推送! 来源:h ...
- 你可能会问,为什么不直接进入 CLOSED 状态,而要停留在 TIME_WAIT 这个状态?
你可能会问,为什么不直接进入 CLOSED 状态,而要停留在 TIME_WAIT 这个状态? 划重点,2MSL 的时间是从主机 1 接收到 FIN 后发送 ACK 开始计时的:如果在 TIME_WAI ...
- libevent之基于socket的bufferevent
基于socket的bufferevent由一个socket的传输层和read/write buffer组成.区别于常规的event,当socket可读或者可写时会回调用户的callback,buffe ...