乐趣区

关于python:探究Python源码终于弄懂了字符串驻留技术

摘要 :在本文中,咱们将深入研究 Python 的外部实现,并理解 Python 如何应用一种名为字符串驻留(String Interning)的技术,实现解释器的高性能。

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

因为字符串是任何编程语言中不可或缺的一个局部,因而,如果有疾速操作字符串的能力,就能够迅速地进步整体的性能。

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

全文提纲如下:

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

字符串驻留是一种编译器 / 解释器的优化办法,它通过缓存一般性的字符串,从而节俭字符串解决工作的空间和工夫。

这种优化办法不会每次都创立一个新的字符串正本,而是仅为每个适当的不可变值保留一个字符串正本,并应用指针援用之。每个字符串的惟一拷贝被称为它的 intern,并因而而得名 String Interning。

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

参考资料

  • 字符串驻留 (https://en.wikipedia.org/wiki…
  • CPython 优化 (https://stummjr.org/post/cpyt…
  • Python 对象第三局部:字符串驻留 (https://medium.com/@bdov_/htt…
  • Python 字符串驻留的外部原理 (http://guilload.com/python-st…
  • Python 优化机制:常量折叠 (https://mp.weixin.qq.com/s/p1…
  • join() 办法的神奇用途与 Intern 机制的软肋 (https://mp.weixin.qq.com/s/M2…

本文分享自华为云社区《深刻 Python 解释器源码,我终于搞明确了字符串驻留的原理!》,原文作者:Python 猫。

点击关注,第一工夫理解华为云陈腐技术~

退出移动版