作者: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已经支持中文变量名啦!》

参考材料

python解释器源码下载_深入 Python 解释器源码,我终于搞明白了字符串驻留的原理!...相关推荐

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

    点击上方"小白学视觉",选择加"星标"或"置顶" 重磅干货,第一时间送达 作者:arpit 译者:豌豆花下猫 声明:本翻译是出于交流学习的目 ...

  2. python电脑版软件下载_最新Python for Windows下载地址电脑版-CC软件

    Python for Windows是一种面向对象.解释型计算机编程语言,整个语言在设计上就保持了清晰简单的风格,易读.易维护,深受广大开发者的喜爱.Python完全免费开源,扩展能力强,可移植性高, ...

  3. 跳一跳python源码下载_教程 跳一跳源码

    这个压缩包为跳一跳工具源码 (安卓版) 仅供代码爱好者交流研究, 且不可用作其他用途,否则后果自负!!! 本来想分享一点数据分析领域-回归预测模型解读与实际工作中发挥用处 后来看到朋友圈被跳一跳霸屏, ...

  4. pythonweb项目源码下载_最新Python WEB开发在线教育项目之谷粒教育 软件源码齐全...

    [课程内容]项目准备 01.根据模板页面抽象app 02.app当中模型类(表)的抽象(1) 03.app当中模型类(表)的抽象(2) 04.项目的创建和配置 05.创建其余app配置子路由,创建自主 ...

  5. python群发邮箱软件下载_用python群发电子邮件

    python作为一款编程语言,其用处多多,今天一米软件就来教教大家用python群发电子邮件怎么做. 1. SMTP 服务器介绍 SMTP(Simple Mail Transfer Protocol) ...

  6. python制作物联网控制软件下载_基于Python和Django框架的物联网智能设备管理系统的设计与实现...

    论文写作指导:请加QQ229366758 基于Python和Django框架的物联网智能设备管理系统的设计与实现 作者:未知 摘 要:针对目前日益增多的智能设备提出了兼容性好,稳定性高,易于管理的管理 ...

  7. python中pygame模块下载_基于python中pygame模块的Linux下安装过程(详解)

    pyhthon中pygame模块怎么安装?pyhthon中pygame模块怎么安装?鄙人为初二一名学生,闲来无事 钻研起电这句话还是建议问一下你们代课老师吧,因为你们老师是这方面专家,诺儿那边的话肯定 ...

  8. python写地下城脚本下载_在python下写脚本的方法

    在python下写脚本的方法 发布时间:2020-07-18 11:43:36 来源:亿速云 阅读:68 作者:清晨 这篇文章主要介绍在python下写脚本的方法,文中介绍的非常详细,具有一定的参考价 ...

  9. python 语音朗读软件下载_使用python编写一个语音朗读闹钟功能的示例代码

    想找一个可以播放文字的闹钟找不到,自己写一个更简单.TTS实现由很多种办法,百度等都提供了API接口,但类似百度,需要先注册等一系列动作. 其实windows自带的win32com功能可以简单实现TT ...

  10. python写地下城脚本下载_用python编写脚本下载电子邮件附件

    我有以下脚本:import imaplib import email import os svdir = 'c:/downloads' mail = imaplib.IMAP4('https://ou ...

最新文章

  1. 【AI】caffe使用步骤(四):训练和预测
  2. 从零开始机器学习比赛经验(bird分享)
  3. 使用ML.NET + Azure DevOps + Azure Container Instances打造机器学习生产化
  4. 基于Xml 的IOC 容器-载入<property>的子元素
  5. linux 进程组id 错乱,【Linux】终端,进程组,作业,会话及作业控制
  6. 春运,一场共享发展的旅程
  7. 未能加载文件或程序集 CrystalDecisions.Web Version=10.2.3600解决方法
  8. 2019液晶电视机质量排名前十名
  9. vscode鼠标滚轮调整字体大小
  10. 服务器nvida显卡驱动安装(亲测)
  11. XYQ加密算法——动态可逆加密算法。
  12. Android SearchView
  13. 编写的activeX 控件如何被IE调用?
  14. Python使用网易的SMTP发送邮件554问题的解决
  15. 19、jsp的实质是什么?
  16. 简单实现ProgressBar三色图(带有百分比)
  17. python web框架flask连接mysql数据库操作
  18. STM32学习笔记1----STM32F429系统时钟
  19. cad字体安装_设计师,你的CAD图纸中是否有很多问号?
  20. php面试题遇到的坑,PHP面试题碰到的几个坑。面壁ing

热门文章

  1. python书籍下载网站_Python 精品开源电子书网站
  2. 微运行库2015_【即心修订】[微简]win10专业工作站版64位18363.720全能版+纯净版[驱动/软件/Admin]...
  3. modbus通讯失败_你以为你真的了解Modbus 通信协议?
  4. 【名单回顾】CSP-J2 2019年第二轮入门级获奖名单(仅列北京地区小学生)
  5. java物流管理系统代码_Java物流配送管理系统 Spring 源码下载
  6. David Pozar 微波工程读书笔记(一)
  7. 国内交易平台关闭了,教你如何把Zcash(zec)兑换成人民币
  8. 一页纸商业计划书 (Business Plan) 模板(转载)
  9. CentOS或Linux中,查看Tomcat版本的三种方式
  10. 来自一个包工头的创业故事