上一节已经大体了解 Unicode 对象的两种创建方式,一种是 compact 模式,即 data 紧跟在类型结构之后,另一种 data 是一个独立的内存,通过指针与类型结构关联在一起。Python 对 String 的处理中,大量使用了缓存技术,以降低内存消耗,在整个解释器的生命周期中存在三种常驻对象。
unicode_empty 对象
该对象定义在 Objects/unicodeobject.c:209 行。
/* The empty Unicode object is shared to improve performance. */
static PyObject *unicode_empty = NULL;
#define _Py_INCREF_UNICODE_EMPTY() \
do { \
if (unicode_empty != NULL) \
Py_INCREF(unicode_empty); \
else { \
unicode_empty = PyUnicode_New(0, 0); \
if (unicode_empty != NULL) { \
Py_INCREF(unicode_empty); \
assert(_PyUnicode_CheckConsistency(unicode_empty, 1)); \
} \
} \
} while (0)
这是一个全局对象,第一次调用 _Py_INCREF_UNICODE_EMPTY() 宏的时候,会创建这个对象,之后只会不断的增减 unicode_empty 的引用计数,所有的空 String 对象都指向这个实例。
unicode_latin1 数组
对于那些编码小于 256 的单个字符,则会全部缓存在一个全局数组中。
/* Single character Unicode strings in the Latin-1 range are being
shared as well. */
static PyObject *unicode_latin1[256] = {NULL};
可以通过下面的函数获得这全局拉丁字母表中的对象:
// Objects/unicodeobject.c:2032
static PyObject*
get_latin1_char(unsigned char ch)
{
PyObject *unicode = unicode_latin1[ch];
if (!unicode) {
unicode = PyUnicode_New(1, ch);
if (!unicode)
return NULL;
PyUnicode_1BYTE_DATA(unicode)[0] = ch;
assert(_PyUnicode_CheckConsistency(unicode, 1));
unicode_latin1[ch] = unicode;
}
Py_INCREF(unicode);
return unicode;
}
代码非常简单,需要注意的是 PyUnicode_New 会获得索引计数为 1 的对象实例,之后的 Py_INCREF(unicode); 则会再增加一次引用计数,这意味着这些字符的引用计数永远不会为 0,真的就可以永久存在了。
interned 机制
回顾 UnicodeObject.state.interned,这个标志位的作用即表示当前对象是否已经驻留内存。所有字符串对象都在全局一个全局字典 interned 中保存,其保存方式是 key、value 均为字符串本身,intern 通过下面函数实现,已经删除无关代码:
void
PyUnicode_InternInPlace(PyObject **p)
{
PyObject *s = *p;
PyObject *t;
if (s == NULL || !PyUnicode_Check(s))
return;
// 初始化 interned 字典
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
// 尝试缓存,t 是命中 value
t = PyDict_SetDefault(interned, s, s);
// 如果是新值,那么就再增加一次 t 的索引计数
if (t != s) {
Py_INCREF(t);
Py_SETREF(*p, t);
return;
}
// 如果命中缓存,减去设置 interned 字典时多增加的两个引用计数
Py_REFCNT(s) -= 2;
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
}
Python 通过这三个全局对象减少重复对象的内存消耗,思路与之前处理整数对象时的小整数池类似,通过提供全局共享对象提高内存利用率。