目录
1.PyStringObject与PyString_Type
2.PyStringObject的创建
3.Intern机制
4.字符缓冲池
5.PyStringObject 效率问题
在python中,PyStringObject是对字符串对象的实现,PyStringObject是一个拥有可变长度内存的对象,这一点容易理解,因为“Hi”和“Lian”创建的PyStringObject其内部需要的内存空间不一样大小。同时,PyStringObject也是一个不可变对象,当创建一个PyStringObject对象后,其内部维护的字符串不能再发生改变。
PyStringObject的定义:
typedef struct {
PyObject_VAR_HEAD
long ob_shash;
int ob_sstate;
char ob_sval[1];
} PyStringObject;
在PyStringObject定义我们可以看到,它的头部实际是一个PyObject_VAR_HEAD,其中有一个ob_size变量保存着对象中维护的可变长度内存的大小。ob_sval是一个字符数组,但是它是作为一个字符指针指向一段内存的,这段内存保存着这个字符串对象维护的实际字符串。
同c中的字符串一样,PyStringObject内部维护的字符串在末尾以‘\0’结尾,所以ob_sval指向的是一段长度为ob_size+1个字节的内存,且ob_sval[ob_szie] ='\0'.
ob_shash的作用是缓存对象的hash值,这样可以避免每一次都要重新计算该字符串对象的hash值,如果PyStringObject还没被计算过hash值,那么ob_shash的初始化值为-1。计算一个字符串对象的hash值采用的算法:
static long string_hash(PyStringObject *a)
{
register Py_ssize_t len;
register unsigned char *p;
register long x;
if (a->ob_shash != -1)
return a->ob_shash;
len = a->ob_size;
p = (unsigned char *) a->ob_sval;
x = *p << 7;
while (--len >= 0)
x = (1000003*x) ^ *p++;
x ^= a->ob_size;
if (x == -1)
x = -2;
a->ob_shash = x;
return x;
}
ob_sstate变量标记该对象是否已经过intern机制处理,intern机制是这节非常重要的点。
PyStringObject的类型对象是PyString_Type:
PyTypeObject PyString_Type = {
PyObject_HEAD_INIT(&PyType_Type)
0,
"str",
sizeof(PyStringObject),
sizeof(char),
string_repr, /* tp_repr */
&string_as_number, /* tp_as_number */
&string_as_sequence, /* tp_as_sequence */
&string_as_mapping, /* tp_as_mapping */
(hashfunc)string_hash, /* tp_hash */
string_methods, /* tp_methods */
&PyBaseString_Type, /* tp_base */
.......
string_new, /* tp_new */
PyObject_Del, /* tp_free */
};
我们可以看到,tp_itemsize被设置为sizeof(char),对于python中任意变长对象,tp_itemsize这个域必须设置,它指明了由变长对象保存的元素的单位长度,tp_itemsize和ob_size共同决定了还需要额外申请的内存大小。
python提供了两条路径从c中原生的字符串创建PyStringObject对象:
PyAPI_FUNC(PyObject *) PyString_FromStringAndSize(const char *, Py_ssize_t);
PyAPI_FUNC(PyObject *) PyString_FromString(const char *);
我们先查看PyString_FromString的源码:
PyObject *PyString_FromString(const char *str)
{
register size_t size;
register PyStringObject *op;
assert(str != NULL);
【1】:判断字符串长度
size = strlen(str);
if (size > PY_SSIZE_T_MAX - sizeof(PyStringObject)) {
PyErr_SetString(PyExc_OverflowError,
"string is too long for a Python string");
return NULL;
}
【2】:处理null string
if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
null_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
【3】:处理单字符,从缓存池中获取
if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
【4】:创建新的PyStringObject对象,并初始化
/* Inline PyObject_NewVar */
op = (PyStringObject *)PyObject_MALLOC(sizeof(PyStringObject) + size);
if (op == NULL)
return PyErr_NoMemory();
PyObject_INIT_VAR(op, &PyString_Type, size);
op->ob_shash = -1;
op->ob_sstate = SSTATE_NOT_INTERNED;
Py_MEMCPY(op->ob_sval, str, size+1);
.........
return (PyObject *) op;
}
在【2】中,第一次在一个空字符串基础上创建一个PyStringObject,由于nullstring指针被初始化为NULL,所以python会为这个空字符串建立一个PyStringObject对象,将这个PyStringObject通过intern机制进行共享,然后nullstring指向这个被共享的对象。如果以后python检查到需要一个空字符串创建PyStringObject对象,这是nullstring已经存在,那么直接返回nullstring引用。
如果不是创建空字符串对象,那么接下来需要进行的动作就是申请内存,创建PyStringObject对象。在【4】处可以看出,除了申请内存,还有为字符串数组内的元素申请额外内存,然后将hash值设为-1,将intern标志设置为SSTATE_NOT_INTERNED。最后将str指向的字符串数组拷贝到PyStringObject所维护的空间中。如“Python”PyStringObject对象在内存中的状态:
在创建PyStringObject对象过程中,我们注意到,当字符数组长度为0或者1时需要进行一个动作PyString_InternInPlace,这就是前面提到的intern机制。
PyObject *
PyString_FromString(const char *str)
{
register PyStringObject *op;
......//创建PyStringObject对象
intern共享比较短的PyStringObject对象
if (size == 0) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
nullstring = op;
Py_INCREF(op);
} 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;
}
PyStringObject对象的intern机制的目的是:对于被intern之后的对象,在python运行期间,系统中只有唯一一个与之相对应的PyStringObject对象。当判断两个PyStringObject对象是否相同时,如果他们都被intern了,那么只需要简单检查他们对应的PyObject*是否相同即可。这个机制既节省空间,又简化了PyStringObject对象的比较。
通过下面的例子来考察一下intern机制的必要性:
>>> a = "Python"
>>> b = "Python"
>>> print(id(a),id(b))
4362549296 4362549296
首先我们创建一个PyStringObject对象a,其字符串为“Python”,随后我们再一次为字符串“Python”创建一个PyStringObject对象。通常情况下,python会为我们重新申请内存,创建一个新的PyStringObject对象b,a和b是完全不同的对象,尽管其内部维护的字符数组完全相同。
那就带来了一个问题,如果我们创建100000个“Python”的PyStringObject呢?显然,这样必然会浪费大量的内存。因此python引入了intern机制。在上面的例子中,如果a应用了intern机制,那么之后要创建b的时候,python首先会在系统中记录的已经被intern机制处理了的PyStringObject对象中查找,如果发现该字符数组对应的PyStringObject已经存在,那么就将该对象的引用返回,不再新建一个PyStringObject对象 -----真实答案是否如此呢??。PyString_InternInPlace正是负责完成对一个对象进行intern操作的函数:
void PyString_InternInPlace(PyObject **p)
{
register PyStringObject *s = (PyStringObject *)(*p);
PyObject *t;
对PyStringObject进行类型和状态检查
if (s == NULL || !PyString_Check(s))
Py_FatalError("PyString_InternInPlace: strings only please!");
if (!PyString_CheckExact(s))
return;
if (PyString_CHECK_INTERNED(s))
return;
创建记录经intern机制处理后的PyStringObject的dict
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
【1】:检查PyStringObject对象s是否存在对应的intern后的
PyStringObject对象
t = PyDict_GetItem(interned, (PyObject *)s);
if (t) {
注意这里对引用计数的调整
Py_INCREF(t);
Py_DECREF(*p);
*p = t;
return;
}
【2】:在intern中记录检查PyStringObject对象s
if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
PyErr_Clear();
return;
}
【3】:注意这里对引用计数的调整
s->ob_refcnt -= 2;
【4】:调整s中的intern状态标志
PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}
PyString_InternInPlace首先会进行一系列检查,其中包括亮相检查内容:
从上面的代码中还是不能知道intern是个什么东西,其实intern指向的是PyDict_New创建的一个对象,而PyDict_New创建的是一个PyDictObject对象,也就是python中的dict,c++中的map
当对一个 PyStringObject对象进行intern处理时,首先在interned这个dict中国检查是否有满足一下条件的对象b(b中维护的字符串与a相同),如果存在,那么指向a的PyObject指针指向b,而a的引用计数-1,b的引用计数+1.
下图是如果interned集合中存在b,在对a进行intern操作,原本指向a的PyObject*指针变化情况:
对于被intern机制处理过的PyStringObject对象,python采用特殊的计数机制。在将一个PyStringObject对象a的PyObject作为key和value添加到interned中,此时a的引用计数进行了两次+1,由于设计者规定interned中的a指针不能被视为有效引用,所以在代码【3】处a的计数器-2,否则删除a是永远不可能的。
我们可以预期,在销毁a的同时,会在interned中销毁指向a的指针。这以猜想可以从代码中得出:
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通过避免新建PyStringObject来节约内存是假的。从PyString_FromString我们可以知道,无论如何,一个合法的PyStringObject对象s是会被创建的。而intern机制是在s被创建后才其作用的,通常python在运行时创建一个临时对象temp后,基本上都会调用PyString_InternInPlace对temp进行处理,intern机制会使temp引用计数减为0,从而使temp被销毁。
上一节讲过python为整数准备了整数对象池,python为字符类型也准备了字符缓冲池。python设计者为PyStringObject设计了一个对象池characters:
static PyStringObject *characters[UCHAR_MAX + 1];
如果创建一个单字符PyStringObject对象,会进行一下操作:
PyObject *
PyString_FromString(const char *str)
{
register PyStringObject *op;
......
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中。如下图演示缓存一个字符对应的PyStringObject对象的过程:
python中可以通过+进行字符串拼接,但是性能及其低下,由于PyStringObject是不可变对象,这意味着在拼接时必须新建一个PyStringObject对象。这样如果连接N个PyStringObject对象,需要N-1次内存的申请工作,这无疑严重影响python的执行效率。
字符串+拼接源码:
static PyObject *
string_concat(register PyStringObject *a, register PyObject *bb)
{
register Py_ssize_t size;
register PyStringObject *op;
.....
计算字符串连接后的长度
size = a->ob_size + b->ob_size;
if (a->ob_size < 0 || b->ob_size < 0 ||
a->ob_size > PY_SSIZE_T_MAX - b->ob_size) {
PyErr_SetString(PyExc_OverflowError,
"strings are too large to concat");
return NULL;
}
/* Inline PyObject_NewVar */
if (size > PY_SSIZE_T_MAX - sizeof(PyStringObject)) {
PyErr_SetString(PyExc_OverflowError,
"strings are too large to concat");
return NULL;
}
创建新的PyStringObject对象,其维护的用于存储字符的内存长度为size
op = (PyStringObject *)PyObject_MALLOC(sizeof(PyStringObject) + size);
if (op == NULL)
return PyErr_NoMemory();
PyObject_INIT_VAR(op, &PyString_Type, size);
op->ob_shash = -1;
op->ob_sstate = SSTATE_NOT_INTERNED;
将a,b字符拷贝到新建的PyStringObject中
Py_MEMCPY(op->ob_sval, a->ob_sval, a->ob_size);
Py_MEMCPY(op->ob_sval + a->ob_size, b->ob_sval, b->ob_size);
op->ob_sval[size] = '\0';
return (PyObject *) op;
#undef b
}
对于任意两个PyStringObject对象拼接,就会进行一次内存申请工作。而如果利用PyStringObject对象的join操作,则会进行下面的动作:
static PyObject *
string_join(PyStringObject *self, PyObject *orig)
{
char *sep = PyString_AS_STRING(self);
const Py_ssize_t seplen = PyString_GET_SIZE(self);
PyObject *res = NULL;
char *p;
Py_ssize_t seqlen = 0;
size_t sz = 0;
Py_ssize_t i;
PyObject *seq, *item;
seq = PySequence_Fast(orig, "");
.....
【1】遍历list中每个字符串,累加获取所有字符串长度
for (i = 0; i < seqlen; i++) {
const size_t old_sz = sz;
item = PySequence_Fast_GET_ITEM(seq, i);
sz += PyString_GET_SIZE(item);
if (i != 0)
sz += seplen;
}
创建长度为sz的PyStringObject对象
res = PyString_FromStringAndSize((char*)NULL, sz);
if (res == NULL) {
Py_DECREF(seq);
return NULL;
}
将list中的字符串拷贝到新建的PyStringObject对象中
p = PyString_AS_STRING(res);
for (i = 0; i < seqlen; ++i) {
size_t n;
item = PySequence_Fast_GET_ITEM(seq, i);
n = PyString_GET_SIZE(item);
Py_MEMCPY(p, PyString_AS_STRING(item), n);
p += n;
if (i < seqlen - 1) {
Py_MEMCPY(p, sep, seplen);
p += seplen;
}
}
Py_DECREF(seq);
return res;
}
执行join操作时,会先统计list中有多少个PyStringObject对象,并统计这里PyStringObject所维护的字符串总长度,然后申请内存,将list中的所有PyStringObject对象维护的字符串拷贝到新开辟的内存空间中。N个PyStringObject对象拼接join只需申请一次内存,比+操作节约了N-2次操作,效率的提升非常明显。