详解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 yield使用方法示例
Dec 04 Python
学习python 之编写简单乘法运算题
Feb 27 Python
Python按行读取文件的实现方法【小文件和大文件读取】
Sep 19 Python
Python根据已知邻接矩阵绘制无向图操作示例
Jun 23 Python
Flask和Django框架中自定义模型类的表名、父类相关问题分析
Jul 19 Python
对python requests的content和text方法的区别详解
Oct 11 Python
对python实现模板生成脚本的方法详解
Jan 30 Python
python调用其他文件函数或类的示例
Jul 16 Python
python2 中 unicode 和 str 之间的转换及与python3 str 的区别
Jul 25 Python
15个应该掌握的Jupyter Notebook使用技巧(小结)
Sep 23 Python
Python使用eval函数执行动态标表达式过程详解
Oct 17 Python
python turtle绘图
May 04 Python
Jupyter notebook 更改文件打开的默认路径操作
深入探讨opencv图像矫正算法实战
python正则表达式re.search()的基本使用教程
pandas:get_dummies()与pd.factorize()的用法及区别说明
python spilt()分隔字符串的实现示例
教你用python实现一个无界面的小型图书管理系统
一篇文章带你搞懂Python类的相关知识
You might like
PHP文件上传原理简单分析
2011/05/29 PHP
php 判断字符串中是否包含html标签
2014/02/17 PHP
php ctype函数中文翻译和示例
2014/03/21 PHP
php实现姓名根据首字母排序的类与方法(实例代码)
2018/05/16 PHP
JavaScript isPrototypeOf和hasOwnProperty使用区别
2010/03/04 Javascript
jquery select(列表)的操作(取值/赋值)
2011/03/16 Javascript
解析Jquery中如何把一段html代码动态写入到DIV中(实例说明)
2013/07/09 Javascript
node.js实现逐行读取文件内容的代码
2014/06/27 Javascript
JavaScript中判断整字类型最简洁的实现方法
2014/11/08 Javascript
让javascript加载速度倍增的方法(解决JS加载速度慢的问题)
2014/12/12 Javascript
Jquery 实现checkbox全选方法
2015/01/28 Javascript
JS模拟实现Select效果代码
2015/09/24 Javascript
Json解析的方法小结
2016/06/22 Javascript
Javascript中常见的逻辑题和解决方法
2016/09/17 Javascript
浅谈Angular2 ng-content 指令在组件中嵌入内容
2017/08/18 Javascript
vue2.0模拟锚点的实例
2018/03/14 Javascript
为jquery的ajax请求添加超时timeout时间的操作方法
2018/09/04 jQuery
如何利用nodejs实现命令行游戏
2020/11/24 NodeJs
python实现网页链接提取的方法分享
2014/02/25 Python
浅谈python中对于json写入txt文件的编码问题
2018/06/07 Python
django 外键model的互相读取方法
2018/12/15 Python
python 堆和优先队列的使用详解
2019/03/05 Python
python中下标和切片的使用方法解析
2019/08/27 Python
Python 使用threading+Queue实现线程池示例
2019/12/21 Python
Python sep参数使用方法详解
2020/02/12 Python
HTML5对比HTML4的主要改变和改进总结
2016/05/27 HTML / CSS
前端实现弹幕效果的方法总结(包含css3和canvas的实现方式)
2018/07/12 HTML / CSS
explicit和implicit的含义
2012/11/15 面试题
Servlet的生命周期
2013/08/25 面试题
行政专员岗位职责
2014/01/02 职场文书
办理生育手续介绍信
2014/01/14 职场文书
《中国的气候》教学反思
2014/02/23 职场文书
交通事故赔偿协议书
2014/04/15 职场文书
清明扫墓感想
2015/08/11 职场文书
Python 中的 copy()和deepcopy()
2021/11/07 Python
Win11 Beta 22621.601 和 22622.601今日发布 KB5017384修复内容汇总
2022/09/23 数码科技