您当前的位置:首页 > 计算机 > 编程开发 > Python

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

时间:12-14来源:作者:点击数:

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;
    }
/* 申请内存空间 */
    res = PyString_FromStringAndSize((char*)NULL, (int)sz);
    /* 连接list中的每一个PyStringObject对象*/
    p = PyString_AS_STRING(res);
for (i = 0; i < seqlen; ++i){
        size_t n;
        /* 获得list中的一个PyStringObject对象*/
        item = PySequence_Fast_GET_ITEM(seq, i);
        n = PyString_GET_SIZE(item);
        memcpy(p, PyString_AS_STRING(item), n);
        p += n;
        if (i < seqlen - 1) 
        {
            memcpy(p, sep, seplen);
            p += seplen;
        }
    }
    Py_DECREF(seq);
    return res;
}

执行join操作时,会首先统计出在list中一共有多少个PyStringObject对象,并统计这些PyStringObject对象所维护的字符串一共有多长,然后申请内存,将list中所有的PyStringObject对象维护的字符串都拷贝到新开辟的内存空间中。注意,这里只进行了一次内存空间的申请,就完成了N个PyStringObject对象的连接操作。相比于“+”操作符,待连接的PyStringObject对象越多,效率的提升也越明显。

6. Hack PyStringObject

在这一节,我们对PyStringObject在运行时行为进行两项观察。

首先,观察Intern机制,在Python Interactive环境中,创建一个PyStringObject后,就会对这个PyStringObject对象进行Intern操作,所以我们预期内容相同的PyStringObject对象在Intern后应该是同一个对象,图4是观察的结果:

通过在string_length中添加打印地址的代码,我们可以在运行时获得每一个PyStringObject对象的地址,从观察结果中可以看到,无论是对于一般的字符串,还是对于单个字符,Intern机制最终都使不同的PyStringObject指针指向了相同的对象。

然后,我们观察Python中进行缓冲处理的字符对象,同样是通过在string_length中添加代码,打印出缓冲池中从a到e的字符对象的引用计数信息。需要注意的是,为了避免执行len(…)对引用计数的影响,我们并不会对a到e的字符对象调用len操作,而是对另外的PyStringObject对象调用len操作:

static void ShowCharater() 
{ 
   char a = 'a'; 
   PyStringObject** posA = characters+(unsigned short)a; 
   int i; 
   for(i = 0; i < 5; ++i) 
   { 
      PyStringObject* strObj = posA[i]; 
      printf("%s, %d\n", strObj->ob_sval, strObj->ob_refcnt); 
   } 
}

图5展示了观察的结果,可以看到,在创建字符对象时,Python 确实只是使用了缓冲池里的对象,而没有创建新的对象。

方便获取更多学习、工作、生活信息请关注本站微信公众号城东书院 微信服务号城东书院 微信订阅号
推荐内容
相关内容
栏目更新
栏目热门
本栏推荐