[绝对原创 转载请注明出处]
字符串对象,在任何一门主流编程语言中,都是整数对象之外使用最广泛的对象。它和整数对象犹如少林,武当,双峰对峙。本章将研究Python中的字符串对象的实现,同整数对象一样,字符串对象的实现中采用了很多额外的机制来保证性能的优化。
本章内容分为三个部分:
1. 研究Python中的字符串对象PyStringObject
2. 研究字符串对象的效率加速机制
3. 分析字符串连接操作的效率,Hack PyStringObject
Python源码剖析
——字符串对象PyStringObject(1)
本文作者: Robert Chen([email protected] )
在对PyIntObject的分析中,我们看到了Python中的具有不可变长度数据的对象(定长对象)。在Python中,还大量存在着另一种对象,即具有可变长度数据的对象(变长对象)。与定长对象不同,对于变长对象而言,对象维护的数据的长度在对象定义时是不知道的。对于PyIntObject来说,其维护的数据的长度在对象定义时就已经确定了,是一个long变量的长度;而可变对象维护的数据的长度只能在对象创建时才能确定,考虑一下,我们只能在创建一个字符串或一个列表时才知道它们所维护的数据的长度,在此之前,对这个信息,我们一无所知。在变长对象中,实际上还可分为可变对象(mutable)和不可变对象(immutable),可变对象是在对象创建之后,其维护的数据的长度还能变化的对象,比如一个list被创建后,可以向其中添加元素或删除元素,这些操作都会改变其维护的数据的长度;而不可变对象所维护的数据在对象创建之后就不能再改变了,比如Python中的string和tuple,它们都不支持添加或删除的操作。本章我们将研究Python变长对象中的不可变对象——字符串对象。
在Python中,PyStringObject是对字符串对象的抽象和表示。PyStringObject是一个拥有可变长度内存的对象,这一点非常容易理解,因为对于表示”Hi”和”Python”的两个不同的PyStringObject对象,其内部需要的保存字符串内容的内存空间显然是不一样的。但同时,PyStringObject对象又是一个不变对象(Immutable)。当创建了一个PyStringObject对象之后,该对象内部维护的字符串就不能再被改变了。这一点特性使得PyStringObject对象能作为PyDictObject的键值,但同时也使得一些字符串操作的效率大大降低,比如多个字符串的连接操作。
PyStringObject对象的声明如下:
[stringobject.h]
typedef struct {
PyObject_VAR_HEAD
long ob_shash;
int ob_sstate;
char ob_sval[1];
} PyStringObject;
在PyStringObject的定义中我们看到,在PyStringObject的头部实际上是一个PyObject_VAR_HEAD,其中有一个ob_size变量保存着对象中维护的可变长度内存的长度。虽然在PyStringObject的定义中,ob_sval是一个字符的字符数组。但是ob_sval实际上是作为一个字符指针指向了一段内存,这段内存保存着这个字符串对象所维护的实际字符串,显然,这段内存不会只是一个字节。而这段内存的实际长度(字节),正是由ob_size来维护,这个机制是Python中所有拥有可变长度内存的对象的实现机制。比如对于PyStringObject对象”Python”,ob_size的值就是6。
同C中的字符串一样,PyStringObject内部维护的字符串在末尾必须以’/0’结尾,但是由于字符串的实际长度是由ob_size维护的,所以PyStringObject表示的字符串对象中间是可能出现字符’/0’的,这一点与C语言中不同,因为在C中,只要遇到了字符’/0’,就认为一个字符串结束了。所以,实际上,ob_sval指向的是一段长度为ob_size+1个字节的内存,而且必须满足ob_sval[ob_size] = ‘/0’。
PyStringObject中的ob_shash变量其作用是缓存该对象的HASH值,这样可以避免每一次都重新计算该字符串对象的HASH值。如果一个PyStringObject对象还没有被计算过HASH值,那么ob_shash的初始值是-1。在计算一个对象的HASH值时,采用如下的算法:
[stringobject.c]
static long string_hash(PyStringObject *a)
{
register int 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;
}
PyStringObject对象的ob_sstate变量该对象是否被Intern的标志,关于PyStringObject的Intern机制,在后面会详细介绍。
下面看一下PyStringObject对应的类型对象:
[stringobject.c]
PyTypeObject PyString_Type = {
PyObject_HEAD_INIT(&PyType_Type)
0,
"str",
sizeof(PyStringObject),
sizeof(char),
……
(reprfunc)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 */
0, /* tp_call */
……
string_new, /* tp_new */
PyObject_Del, /* tp_free */
};
可以看到,在PyStringObject的类型对象中,tp_itemsize被设置为sizeof(char),即一个字节。对于Python中的任何一种变长对象,tp_itemsize这个域是必须设置的,tp_itemsize指明了由变长对象保存的元素的单位长度,所谓单位长度即是指一个对象在内存中的长度。这个tp_itemsize和ob_size共同决定了应该额外申请的内存的总大小是多少。
需要注意的是,我们看到,tp_as_number,tp_as_sequence,tp_as_mapping,三个域都被设置了。这表示PyStringObject对数值操作,序列操作和映射操作都支持。
Python提供两条路径,从C中原生的字符串创建PyStringObject对象。我们先考察一下最一般的PyString_FromString:
[stringobject.c]
PyObject *
PyString_FromString(const char *str)
{
register size_t size;
register PyStringObject *op;
assert(str != NULL);
/*判断字符串长度*/
size = strlen(str);
if (size > INT_MAX) {
PyErr_SetString(PyExc_OverflowError,
"string is too long for a Python string");
return NULL;
}
/*处理null string*/
if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
null_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
/* 创建新的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;
memcpy(op->ob_sval, str, size+1);
/* Itern(共享)长度较短的PyStringObject对象 */
if (size == 0) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
nullstring = op;
Py_INCREF(op);
} else if (size == 1) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
}
显然,传给PyString_FromString的参数必须是一个指向以NULL结尾的字符串的指针。在从一个原生字符串创建PyStringObject时,首先需要检查该字符数组的长度,如果字符数组的长度大于了MAX_INT,那么Python将不会创建对应得PyStringObject对象。MAX_INT在系统的头文件中定义,是一个平台相关的值,在WIN32系统下,该值为:
#define INT_MAX 2147483647
嗯,这个界限值确实非常庞大了,如果不是由于变态,几乎没有人会去试图超越这个禁区的 :)
接下来,将会检查传入的字符串是不是一个空串,对于空串,Python并不是每一次都会创建相应得PyStringObject。Python运行时有一个PyStringObject对象指针nullstring专门负责处理空的字符数组。如果第一次在一个空字符串基础上创建PyStringObject,由于nullstring指针被初始化为NULL,所以Python会为这个空字符建立一个PyStringObject对象,将这个PyStringObject对象通过Intern机制进行共享,然后将nullstring指向这个被共享的对象。如果在以后Python检查到需要为一个空字符串创建PyStringObject对象,这时nullstring已经存在了,那么就直接返回nullstring的引用。
接下来需要进行的动作就是申请内存,创建PyStringObject对象。可以看到,这里申请的内存除了PyStringObject的内存,还有为字符数组内的元素申请的额外的内存。然后,将HASH缓存值设为-1,将Intern标志设为SSTATE_NOT_INTERNED。最后将字符数组内的字符拷贝到PyStringObject所维护的空间中,在拷贝的过程中,将字符数组最后的’/0’字符也拷贝了。加入我们对于字符数组”Python”建立PyStringObject对象,那么对象建立完成后在内存中的状态如图1所示:
在PyString_FromString之外,还有一条创建PyStringObject对象的途径:PyString_FromStringAndSize:
[stringobject.c]
PyObject* PyString_FromStringAndSize(const char *str, int size)
{
register PyStringObject *op;
/*处理null string*/
if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
null_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
if (size == 1 && str != NULL &&
(op = characters[*str & UCHAR_MAX]) != NULL)
{
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
/* 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;
if (str != NULL)
memcpy(op->ob_sval, str, size);
op->ob_sval[size] = '/0';
/* share short strings */
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;
}
PyString_FromStringAndSize的操作过程和PyString_FromString一般无二,只是有一点,PyString_FromString传入的参数必须是以NULL结尾的字符数组的指针,而PyString_FromStringAndSize不会有这样的要求,因为通过传入的size参数就可以确定需要拷贝的字符的个数。