[绝对原创 转载请注明出处]
Python源码剖析
——字符串对象PyStringObject(3)
本文作者: Robert Chen(search.pythoner@gmail.com )
5. PyStringObject效率相关问题
关于PyStringObject,有一个地球人都知道的严重影响Python程序执行效率的问题,有一种说法,绝大部分执行效率特别低下的Python程序都是由于没有注意到这个问题所致。下面我们就来看看这个在Python中举足轻重的问题——字符串连接。
假如现在有两个字符串“Python”和“Ruby”,在Java或C#中,都可以通过使用“+”操作符,将两个字符串连接在一起,得到一个新的字符串“PythonRuby”。当然,Python中同样提供了利用“+”操作符连接字符串的功能,然而不幸的是,这样的做法正是万恶之源。
Python中通过“+”进行字符串连接的方法效率及其低下,其根源在于Python中的PyStringObject对象是一个不可变对象。这就意味着当进行字符串连接时,实际上是必须要创建一个新的PyStringObject对象。这样,如果要连接N个PyStringObject对象,那么就必须进行N-1次的内存申请及内存搬运的工作。这将严重影响Python的执行效率。
官方推荐的做法是通过利用PyStringObject对象的join操作来对存储在list或tuple中的一组PyStringObject对象进行连接操作,这种做法只需要分配一次内存,执行效率将大大提高。
下面我们通过考察源码来更细致地了解这一问题。
通过“+”操作符对字符串进行连接时,会调用string_concat函数:
[stringobject.c]
static PyObject* string_concat(register PyStringObject *a, register PyObject *bb)
{
register unsigned int size;
register PyStringObject *op;
#define b ((PyStringObject *)bb)
……
size = a->ob_size + b->ob_size;
/* 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, a->ob_sval, (int) a->ob_size);
memcpy(op->ob_sval + a->ob_size, b->ob_sval, (int) b->ob_size);
op->ob_sval[size] = '/0';
return (PyObject *) op;
#undef b
}
对于任意两个PyStringObject对象的连接,就会进行一次内存申请的动作。而如果利用PyStringObject对象的join操作,则会进行如下的动作(假设是对list中的PyStringObject对象进行连接):
[stringobject.c]
static PyObject* string_join(PyStringObject *self, PyObject *orig)
{
char *sep = PyString_AS_STRING(self);
const int seplen = PyString_GET_SIZE(self);
PyObject *res = NULL;
char *p;
int seqlen = 0;
size_t sz = 0;
int i;
PyObject *seq, *item;
。。。。。。//获得list中PyStringObject对象的个数,保存在seqlen中
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;
}
/* 申请内存空间