详解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中的fileinput模块的简单实用示例
Jul 09 Python
用python处理图片之打开\显示\保存图像的方法
May 04 Python
python方法生成txt标签文件的实例代码
May 10 Python
Python I/O与进程的详细讲解
Mar 08 Python
django获取from表单multiple-select的value和id的方法
Jul 19 Python
python中几种自动微分库解析
Aug 29 Python
python保留小数位的三种实现方法
Jan 07 Python
Python读写操作csv和excle文件代码实例
Mar 16 Python
python实现坦克大战
Apr 24 Python
Python操作Excel把数据分给sheet
May 20 Python
在keras中model.fit_generator()和model.fit()的区别说明
Jun 17 Python
在Keras中CNN联合LSTM进行分类实例
Jun 29 Python
Jupyter notebook 更改文件打开的默认路径操作
深入探讨opencv图像矫正算法实战
python正则表达式re.search()的基本使用教程
pandas:get_dummies()与pd.factorize()的用法及区别说明
python spilt()分隔字符串的实现示例
教你用python实现一个无界面的小型图书管理系统
一篇文章带你搞懂Python类的相关知识
You might like
Javascript中的Split使用方法与技巧
2007/03/09 Javascript
JQuery实现的在新窗口打开链接的方法小结
2010/04/22 Javascript
qTip 基于JQuery的Tooltip插件[兼容性好]
2010/09/01 Javascript
js的一些常用方法小结
2011/06/29 Javascript
javascript 中__proto__和prototype详解
2014/11/25 Javascript
javascript实现根据3原色制作颜色选择器的方法
2015/07/17 Javascript
JQuery Mobile 弹出式登录框的实现方法
2016/05/28 Javascript
利用jQuery插件imgAreaSelect实现获得选择域的图像信息
2016/12/02 Javascript
详解node.js平台下Express的session与cookie模块包的配置
2017/04/26 Javascript
vue中遇到的坑之变化检测问题(数组相关)
2017/10/13 Javascript
Angular 组件之间的交互的示例代码
2018/03/24 Javascript
vue使用rem实现 移动端屏幕适配
2018/09/26 Javascript
js实现点击展开隐藏效果(实例代码)
2018/09/28 Javascript
vue 地图可视化 maptalks 篇实例代码详解
2019/05/21 Javascript
node 标准输入流和输出流代码实例
2019/09/19 Javascript
JavaScript享元模式原理与用法实例详解
2020/03/09 Javascript
jquery实现轮播图特效
2020/04/12 jQuery
Js生成随机数/随机字符串的方法小结【5种方法】
2020/05/27 Javascript
Vue实现简单购物车功能
2020/12/13 Vue.js
[32:36]完美世界DOTA2联赛PWL S3 LBZS vs CPG 第二场 12.12
2020/12/16 DOTA
Python性能优化的20条建议
2014/10/25 Python
Python实现简单截取中文字符串的方法
2015/06/15 Python
Selenium 模拟浏览器动态加载页面的实现方法
2018/05/16 Python
Python之使用adb shell命令启动应用的方法详解
2019/01/07 Python
python环境路径配置以及命令行运行脚本
2019/04/02 Python
Python中base64与xml取值结合问题
2019/12/22 Python
Python 开发工具通过 agent 代理使用的方法
2020/09/27 Python
Python根据字典的值查询出对应的键的方法
2020/09/30 Python
5 分钟读懂Python 中的 Hook 钩子函数
2020/12/09 Python
python实现发送QQ邮件(可加附件)
2020/12/23 Python
CSS实现聊天气泡效果
2020/04/26 HTML / CSS
Strathberry苏贝瑞中国官网:西班牙高级工匠手工打造
2020/10/19 全球购物
女大学生自我鉴定
2013/12/09 职场文书
《再别康桥》教学反思
2014/02/12 职场文书
Mac电脑OS系统下安装Nginx的详细教程
2022/04/14 Servers
MySQL主从切换的超详细步骤
2022/06/28 MySQL