Python 源码剖析 - 字符串对象 PyStringObject(2)

发布于 2023-01-23 20:37:45 字数 8142 浏览 4 评论 0

3. Intern 机制

无论是PyString_FromString还是PyString_FromStringAndSize,我们都注意到,当字符数组的长度为0或1时,需要进行了一个特别的动作:PyString_InternInPlace。这就是前面所提到的Intern机制。

PyStringObject对象的Intern机制其目的是对于被Intern之后的字符串,在整个Python运行时,系统中都只有唯一的与该字符串对应的PyStringObject对象。这样当判断两个PyStringObject对象是否相同时,如果它们都被Intern了,那么只需要简单地检查它们对应的PyObject*是否相同即可。这个机制既节省了空间,又简化了对PyStringObject对象的比较,嗯,可谓是一箭双雕哇。

假如在某个时刻,我们创建了一个PyStringObject对象A,其表示的字符串是“Python”,在之后的某一时刻,加入我们想为“Python”再次建立一个PyStringObject对象,通常情况下,Python会为我们重新申请内存,创建一个新的PyStringObject对象B,A与B是完全不同的两个对象,尽管其内部维护的字符数组是完全相同的。

这就带来了一个问题,加入我们在程序中创建了100个“Python”的PyStringObject对象呢?显而易见,这样会大量地浪费珍贵的内存。因此Python对PyStringObject对象引入了Intern机制。在上面的例子中,如果对于A应用了Intern机制,那么之后要创建B的时候,Python会首先在系统中记录的已经被Intern的PyStringObject对象中查找,如果发现该字符数组对应的PyStringObject对象已经存在了,那么就将该对象的引用返回,而不会创建对象B。PyString_InternInPlace正是完成对一个对象的Intern操作:

[stringobjec.c]
void PyString_InternInPlace(PyObject **p)
{
register PyStringObject *s = (PyStringObject *)(*p);
PyObject *t;
if (s == NULL || !PyString_Check(s))
Py_FatalError("PyString_InternInPlace: strings only please!");
/* If it's a string subclass, we don't really know what putting
it in the interned dict might do. */
if (!PyString_CheckExact(s))
return;
if (PyString_CHECK_INTERNED(s))
return;
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
t = PyDict_GetItem(interned, (PyObject *)s);
if (t) {
Py_INCREF(t);
Py_DECREF(*p);
*p = t;
return;
}
if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
PyErr_Clear();
return;
}
/* The two references in interned are not counted by refcnt.
The string deallocator will take care of this */
s->ob_refcnt -= 2;
PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}

首先会进行一系列的检查。首先,会检查传入的对象是否是一个PyStringObject对象,Intern机制只能应用在PyStringObject对象上,甚至对于它的派生类对象系统都不会应用Intern机制。然后,会检查传入的PyStringObject对象是否已经被Intern机制处理过了,Python不会对同一个PyStringObject对象进行一次以上的Intern操作。
从代码中我们可以清楚地看到,Intern机制的核心在于interned这个东西,那么这个东西是个什么东西呢?

static PyObject *interned; 

从stringobject.c中的定义我们完全不知道interned是个什么东西,然而在这里我们看到,interned实际上指向的是PyDict_New创建的一个对象。而PyDict_New实际上创建了一个PyDictObject对象,这个对象我们将在后面详细地剖析。其实,现在,一个PyDictObject对象完全可以看作是C++中的map,即map<PyObject*, PyObject*>。

现在一切都清楚了,所谓的Intern机制,实际上就是系统中有一个(Key, Value)的映射的集合interned。在这个集合中,记录着被应用了Intern机制的PyStringObject对象。当对一个PyStringObject对象A应用Intern机制时,首先会在Interned中检查是否有满足一下条件的对象B:B中维护的原生字符串与A相同。如果确实存在对象B,那么指向A的PyObject指针将会指向B,而A的引用计数减1,这样,其实A只是一个临时被创建的对象。如果interned中还不存在这样的B,那么就将A记录到interned中。

图2展示了如果Interned中存在这样的对象B,在对A进行Intern操作时, 原本指向A的PyObject指针的变化:

对于被Intern的PyStringObject对象,Python采用了特殊的引用计数机制。在将一个PyStringObject对象A的PyObject指针作为Key和Value添加到interned中时,PyDictObject对象会通过这两个指针对A的引用计数进行两次加1操作。但是Python的设计者规定在interned中A的指针不能被视为对象A的有效引用,因为如果是有效引用的话,那么A的引用计数在Python运行时结束之前永远都不可能为0,因为至少有interned中的两个指针引用了A,那么删除A就永远不可能,这显然是没有道理的。

因此interned中的指针不能作为A的有效引用。这也就是在PyString_InternInPlace最后会将引用计数减2的原因。当A的引用计数在某个时刻减为0之后,系统将会销毁对象A,那么我们可以预期,在销毁A的同时,会在interned中删除指向A的指针,显然,这一点在string_dealloc得到了验证:

[stringobject.c] 
static void string_dealloc(PyObject *op)
{
switch (PyString_CHECK_INTERNED(op)) {
case SSTATE_NOT_INTERNED:
break;
case SSTATE_INTERNED_MORTAL:
/* revive dead object temporarily for DelItem */
op->ob_refcnt = 3;
if (PyDict_DelItem(interned, op) != 0)
Py_FatalError(
"deletion of interned string failed");
break;
case SSTATE_INTERNED_IMMORTAL:
Py_FatalError("Immortal interned string died.");
default:
Py_FatalError("Inconsistent interned string state.");
}
op->ob_type->tp_free(op);
}

前面提到,Python在创建一个字符串时,会首先在interned中检查是否已经有该字符串对应得PyStringObject对象了,如果有,则不用创建新的,这样可以节省内存空间。事到如今,我必须要承认,我说谎了,节省内存空间是没错的,可是Python并不是在创建PyStringObject时就通过interned实现了节省空间的目的。事实上,从 PyString_FromString 中可以看到,无论如何,一个合法的PyString_FromString对象是会被创建的,同样,我们可以注意到,PyString_InternInPlace也只对PyStringObject起作用。

事实正是如此,Python始终会为字符串S创建PyStringObject对象,尽管S在interned中已经有一个与之对应的PyStringObject对象了。而Intern机制是在S被创建后才起作用的,通常Python在运行时创建了一个PyStringObject对象Temp后,基本上都会调用PyString_InternInPlace,Intern机制会减少Temp的引用计数,Temp对象会由于引用计数减为0 而被销毁,它只是作为一个临时对象昙花一现地在内存中闪现,然后湮灭。

那么我们现在有一个疑问了,是否可以直接在C的原生字符串上做Intern的动作,而不需要再创建这样一个临时对象呢?事实上,Python确实提供了一个以char*为参数的Intern机制相关函数,但是你会相当失望,嗯,因为它基本上是换汤不换药的:

[stringobject.c]
PyObject* PyString_InternFromString(const char *cp)
{
PyObject *s = PyString_FromString(cp);
if (s == NULL)
return NULL;
PyString_InternInPlace(&s);
return s;
}

临时对象照样被创建出来,实际上,仔细一想,就会发现在Python中,必须创建这样一个临时的PyStringObject对象来完成Intern操作。为什么呢?答案就在PyDictObject对象interned中,因为PyDictObject必须以PyObject指针作为键。

关于PyStringObject对象的Intern机制,还有一点需要注意。实际上,被Intern的PyStringObject对象分为两类,一类是SSTATE_INTERNED_IMMORTAL状态的,而另一类是SSTATE_INTERNED_MORTAL状态的,这两种状态的区别在string_dealloc中可以清晰地看到,显然,SSTATE_INTERNED_IMMORTAL状态的PyStringObject对象是永远不会被销毁的,它将与Python run time同年同月同日死。

PyString_InternInPlace只能创建SSTATE_INTERNED_MORTAL状态的PyStringObject对象,如果想创建SSTATE_INTERNED_IMMORTAL状态的对象,必须要通过另外地接口,在调用了PyString_InternInPlace后,强制改变PyStringObject的intern状态。

[stringobject.c] 
void PyString_InternImmortal(PyObject **p)
{
PyString_InternInPlace(p);
if (PyString_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {
PyString_CHECK_INTERNED(*p) = SSTATE_INTERNED_IMMORTAL;
Py_INCREF(*p);
}
}

4. 字符缓冲池

最后需要注意的一点是与PyIntObject中的小整数对象的对象池一样,Python的设计者为PyStringObject中的一个字节的字符对象也设计了这样一个对象池characters。

static PyStringObject *characters[UCHAR_MAX + 1]; 

其中的UCHAR_MAX是在系统头文件中定义的常量,这也是一个平台相关的常量,在Win32平台下:

#define UCHAR_MAX 0xff /* maximum unsigned char value */ 

在Python的整数对象体系中,小整数的缓冲池是在Python runtime初始化时被创建的,而字符串对象体系中的字符缓冲池则是以静态变量的形式存在着的。在Python runtime初始化完成之后,缓冲池中的所有PyStringObject指针都为空。

在创建一个PyStringObject时,无论是调用PyString_FromString还是PyString_FromStringAndSize,在创建的字符串实际上是一个字符时,会进行如下的操作:

[stringobject.c]
PyObject* PyString_FromStringAndSize(const char *str, int size)
{
。。。。。。
else if (size == 1 && str != NULL)
{
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
}

先对所创建的字符串(字符)对象进行Intern操作,再将Intern的结果缓存到字符缓冲池characters中。图3演示了缓存一个字符对象的过程。

3条带有标号的曲线既代表指针,又代表进行操作的顺序:

1) 创建PyStringObject对象”P”
2) 对对象”P”进行Intern操作
3) 将对象”P”缓存至字符缓冲池中

同样,在创建PyStringObject时,会首先检查所要创建的是否是一个字符对象,然后检查字符缓冲池中是否已经有了这个字符的字符对象的缓冲,如果有,则直接返回这个缓冲的对象即可:

[stringobject.c]
PyObject* PyString_FromStringAndSize(const char *str, int size)
{
register PyStringObject *op;
……
if (size == 1 && str != NULL &&
(op = characters[*str & UCHAR_MAX]) != NULL)
{
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
……
}

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

列表为空,暂无数据

关于作者

JSmiles

生命进入颠沛而奔忙的本质状态,并将以不断告别和相遇的陈旧方式继续下去。

0 文章
0 评论
84935 人气
更多

推荐作者

    原文
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击“接受”或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。