详解python字符串驻留技术


Posted in Python onMay 21, 2021

前言

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

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

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

全文提纲如下:

详解python字符串驻留技术

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

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

这种优化方法不会每次都创建一个新的字符串副本,而是仅为每个适当的不可变值保留一个字符串副本,并使用指针引用之。每个字符串的唯一拷贝被称为它的intern,并因此而得名 String Interning。

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

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

详解python字符串驻留技术

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 中标识符的命名规则,在 Python2 版本只有“字母、数字和下划线”,但在 Python 3.x 版本中,已经支持 Unicode 编码。这部分内容推荐阅读《醒醒!Python已经支持中文变量名啦!》

以上就是详解python字符串驻留技术的详细内容,更多关于python字符串驻留技术的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
Python遍历目录的4种方法实例介绍
Apr 13 Python
Python栈类实例分析
Jun 15 Python
python如何实现远程控制电脑(结合微信)
Dec 21 Python
python批量获取html内body内容的实例
Jan 02 Python
解决安装python3.7.4报错Can''t connect to HTTPS URL because the SSL module is not available
Jul 31 Python
Python爬取YY评级分数并保存数据实现过程解析
Jun 01 Python
keras topN显示,自编写代码案例
Jul 03 Python
Python3+selenium配置常见报错解决方案
Aug 28 Python
基于Python的身份证验证识别和数据处理详解
Nov 14 Python
Django多个app urls配置代码实例
Nov 26 Python
Python开发之QT解决无边框界面拖动卡屏问题(附带源码)
May 27 Python
Python import模块的缓存问题解决方案
Jun 02 Python
Jupyter notebook 更改文件打开的默认路径操作
深入探讨opencv图像矫正算法实战
python正则表达式re.search()的基本使用教程
pandas:get_dummies()与pd.factorize()的用法及区别说明
python spilt()分隔字符串的实现示例
教你用python实现一个无界面的小型图书管理系统
一篇文章带你搞懂Python类的相关知识
You might like
PHP 获取远程网页内容的代码(fopen,curl已测)
2011/06/06 PHP
php中session过期时间设置及session回收机制介绍
2014/05/05 PHP
PHP+JS实现的实时搜索提示功能
2018/03/13 PHP
通过身份证号得到出生日期和性别的js代码
2009/11/23 Javascript
GWT中复制到剪贴板 js+flash实现复制 兼容性比较好
2010/03/07 Javascript
javascript解三阶幻方(九宫格)
2015/04/22 Javascript
jquery Validation表单验证使用详解
2020/09/12 Javascript
jQuery实现扑克正反面翻牌效果
2017/03/10 Javascript
使用vue与jquery实时监听用户输入状态的操作代码
2017/09/19 jQuery
利用Three.js如何实现阴影效果实例代码
2017/09/26 Javascript
jQuery EasyUI Layout实现tabs标签的实例
2017/09/26 jQuery
vue2.0父子组件间传递数据的方法
2018/08/16 Javascript
微信小程序云开发如何使用npm安装依赖
2019/05/18 Javascript
微信打开网址添加在浏览器中打开提示的办法
2019/05/20 Javascript
vue使用高德地图根据坐标定位点的实现代码
2019/08/22 Javascript
微信小程序实现购物车代码实例详解
2019/08/29 Javascript
在vue中利用v-html按分号将文本换行的例子
2019/11/14 Javascript
使用Vue生成动态表单
2019/11/26 Javascript
Servlet返回的数据js解析2种方法
2019/12/12 Javascript
JavaScript工具库MyTools详解
2020/01/01 Javascript
JS sort方法基于数组对象属性值排序
2020/07/10 Javascript
vscode+gulp轻松开发小程序的完整步骤
2020/10/18 Javascript
[04:55]完美世界副总裁蔡玮:DOTA2的自由、公平与信任
2013/12/18 DOTA
python dict 相同key 合并value的实例
2019/01/21 Python
docker-py 用Python调用Docker接口的方法
2019/08/30 Python
基于Python生成个性二维码过程详解
2020/03/05 Python
基于Tensorflow读取MNIST数据集时网络超时的解决方式
2020/06/22 Python
Numpy中np.random.rand()和np.random.randn() 用法和区别详解
2020/10/23 Python
Django中ORM的基本使用教程
2020/12/22 Python
饮料业务员岗位职责
2013/12/15 职场文书
北京大学自荐信范文
2014/01/28 职场文书
刘公岛导游词
2015/02/05 职场文书
婚宴来宾致辞
2015/07/28 职场文书
Nginx已编译的nginx-添加新模块
2021/04/01 Servers
Go语言带缓冲的通道实现
2021/04/26 Golang
MySQL自定义函数及触发器
2022/08/05 MySQL