Python源码剖析[8] —— 字符串对象(3)

    技术2022-05-11  73

    [绝对原创 转载请注明出处]

    Python源码剖析

    ——字符串对象PyStringObject(3)

    本文作者: Robert Chen(search.pythoner@gmail.com )

    5.      PyStringObject效率相关问题

    关于PyStringObject,有一个地球人都知道的严重影响Python程序执行效率的问题,有一种说法,绝大部分执行效率特别低下的Python程序都是由于没有注意到这个问题所致。下面我们就来看看这个在Python中举足轻重的问题——字符串连接。

    假如现在有两个字符串“Python”和“Ruby”,在JavaC#中,都可以通过使用“+”操作符,将两个字符串连接在一起,得到一个新的字符串“PythonRuby”。当然,Python中同样提供了利用“+”操作符连接字符串的功能,然而不幸的是,这样的做法正是万恶之源。

    Python中通过“+”进行字符串连接的方法效率及其低下,其根源在于Python中的PyStringObject对象是一个不可变对象。这就意味着当进行字符串连接时,实际上是必须要创建一个新的PyStringObject对象。这样,如果要连接NPyStringObject对象,那么就必须进行N-1次的内存申请及内存搬运的工作。这将严重影响Python的执行效率。

    官方推荐的做法是通过利用PyStringObject对象的join操作来对存储在listtuple中的一组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;

    。。。。。。//获得listPyStringObject对象的个数,保存在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对象维护的字符串都拷贝到新开辟的内存空间中。注意,这里只进行了一次内存空间的申请,就完成了NPyStringObject对象的连接操作。相比于“+”操作符,待连接的PyStringObject对象越多,效率的提升也越明显。

    6.      Hack PyStringObject

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

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

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

    然后,我们观察Python中进行缓冲处理的字符对象,同样是通过在string_length中添加代码,打印出缓冲池中从ae的字符对象的引用计数信息。需要注意的是,为了避免执行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确实只是使用了缓冲池里的对象,而没有创建新的对象。


    最新回复(0)