[python源码剖析]字符串对象(二)

本文主要阐述三个问题:

  1.intern机制

  2.字符串缓冲池

   3.PyStringObject效率问题

 

一、Intern机制

  什么是intern机制呢?简单来说,就是创建字符串对象的一种快捷方式。先来看看源码

PyObject *PyString_FromStringAndSize(const char *str, Py_ssize_t size)
{
    register PyStringObject *op;
    ........
   //intern机制
    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_FromString,还是PyString_FromStringAndSize,当字符数组的长度为0或1时,都要进行:PyString_InternInPlace。这就是Intern机制。

  其目的是:对于被intern之后的字符串,比如“dog”,在整个python运行期间,系统中只有一个与字符串'dog'对应的PystringObject对象。这样,当判断两个PystringObject对象是否相同时,如果他们都被intern了,那么只需要简单地检查它们对应的pyObject*是否相同即可。这种机制既节省了空间,又简化了PystringObject对象的比较。

  比如:a = 'python',b = 'python'。对象a和b是两个不同的PystringObject对象,但是它们内部维护的字符数组确实完全相同的。如果创建100个'python'的PystringObject对象,显然是很浪费内存的。因此python为PystringObject对象引入了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]:在interned中记录检查PystringObject对象a
                if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
                    PyErr_Clear();
                    return;
                }
                //[3]:对引用计数进行调整
                Py_REFCNT(s) -= 2;
                //[4]:调整s中的intern状态标志
                PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
            }
View Code

  PyString_InternInPlace首先会进行一系列检查,其中包括以下内容:

    检查传入的对象是否是一个PystringObject对象,intern机制只能应用在PystringObject对象上,甚至其派生类对象系统都不会应用intern机制。
    检查传入的PystringObject对象是否已经被intern机制处理过了,python不会对同一个PystringObject对象进行一次以上的intern操作。

  intern机制的核心在于interned。intern机制的关键,就在于系统中有一个(key,value)映射关系的集合。集合的名称叫做interned.在這个集合中,记录着被intern机制处理过的PystringObject对象。当一个PystringObject对象应用intern机制时,首先会在interned這个dict中检查是否满足以下条件的对象b:b中维护的原生字符串与a相同。如果存在,那么指向a的PyObject指针将会指向b,而a的引用计数减一。如果interned中不存在,则就记录到interned中。

  

  对于被intern机制处理的PystringObject对象,python采用了引用计数机制。在将一个PystringObject对象a的pyobject指针作为key和value添加到Intered中时,PystringObject对象会通过这两个指针对a的引用计数进行两次加1操作。

  python设计者规定在interned中a的指针不能被视为对象a的有效引用,因为如果是有效引用的话,那么a的引用计数在python结束之前永远不可能为0,因为interned中至少有两个指针引用了a,那么删除a就永远不可能了。
  因此,interned中的指针不能作为有效引用,这就是上述代码[3]处将引用计数减2的原因。在某个时刻A的引用计数减为0后,系统就会销毁对象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 */
                        Py_REFCNT(op) = 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.");
                }
                Py_TYPE(op)->tp_free(op);
            }  

  python在创建一个字符串时,首先会检查interned中是否已经有该字符串对应的PystringObject对象,如果有则不用创建新的,這样可以节省内存空间。然而事实并非如此。
python始终会为字符串创建PystringObject对象,尽管s中维护的原生字符数组在interned中已经有一个与之对应的PystringObject对象了。

  而interned机制是在s被创建后才起作用的,通常在运行时创建一个PystringObject对象temp后,基本上都会调用PyString_InternInPlace对temp进行处理,intern机制会减少temp的引用计数,temp对象会由于引用计数减少为0而被销毁。python源码中还提供了一种以char*为参数的intern机制函数:

PyObject *PyString_InternFromString(const char *cp){
                    PyObject *s = PyString_FromString(cp);
                    if (s == NULL)
                        return NULL;
                    PyString_InternInPlace(&s);
                    return s;
                }  

  事实上,必须要创建这样一个临时的PystringObject对象来完成intern操作。为什么呢?答案就在PystringObject对象interned中,因为PyDictObject必须以PyObject*指针作为键。被intern机制处理后的PystringObject对象分为两类:一类处于SSTATE_INTERNED_IMMORTAL状态,另一类处于SSTATE_INTERNED_MORTAL。两者区别在string_dealloc中可以看出,SSTATE_INTERNED_IMMORTAL状态的PystringObject对象是永远不会被销毁。
  PyString_InternInPlace只能创建SSTATE_INTERNED_MORTAL状态的PystringObject对象,如果想创建SSTATE_INTERNED_IMMORTAL状态的对象,必须通过另外的接口,在调用了PyString_InternInPlace后,强制改变PystringObject对象的intern状态。

void PyString_InternImmortal(PyObject **p){
                PyString_InternInPlace(p);
                if (PyString_CHECK_INTERNED(*p) != SSTATE_INTERNED_IMMORTAL) {
                    PyString_CHECK_INTERNED(*p) = SSTATE_INTERNED_IMMORTAL;
                    Py_INCREF(*p);
                }
            }

二、字符串缓冲池

  整数对象有专门的缓冲池.类似的,在字符串中也设计了缓冲池.python设计者为PystringObject中的一个字节的字符对应的PystringObject对象也设计了這样一个对象池characters:

  static PyStringObject *characters[UCHAR_MAX + 1];
      其中UCHAR_MAX是系统头文件中定义的常量,在win32平台下:
  #define UCHAR_MAX 0fxx
     整数对象体系中,小整数的缓冲池是在python初始化时被创建的,而字符串对象体系中的字符缓冲池则是以静态变量的形式存在着。在python初始化完成之后,缓冲池中所有PystringObject指针均为空,当我们创建一个PystringObject对象时,无论是PyString_FromString,还是PyString_FromStringAndSize,如果字符串是一个字符,则会进行如下操作:

PyObject *PyString_FromStringAndSize(const char *str, Py_ssize_t size){
                .....
                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对象<string>
    对对象<string p>进行intern操作
    将对象<string p>缓存至字符缓冲池中
  同样,在创建PyStringObject时,会首先检查所要创建的是否是一个字符对象,然后检查字符缓冲池是否已经有了這个字符的字符对象的缓冲,如果有,则直接返回這个缓冲的对象即可:

PyObject *PyString_FromStringAndSize(const char *str, Py_ssize_t size){
                register PyStringObject *op;
                ......
                //处理字符
                if (size == 1 && str != NULL &&
                    (op = characters[*str & UCHAR_MAX]) != NULL)
                {
                #ifdef COUNT_ALLOCS
                    one_strings++;
                #endif
                    Py_INCREF(op);
                    return (PyObject *)op;
                }
                .....
            }
View Code

三、PyStringObject效率问题

  严重影响PyStringObject效率的问题-----字符串连接('+'操作符)

  假如有两个字符串"python"和“ruby”,python提供了+操作符来连接两个字符串。然而這种效率是极为低下的,其根源在于python中的PyStringObject对象是一个不可变对象,这就意味着当进行字符串连接时,实际上是必须要创建一个新的PyStringObject对象。這样,如果要连接N个PyStringObject对象,那么必须要进行N-1次的申请内存及内存搬运工作,毫无疑问影响了执行效率。

  官方推荐做法是利用PyStringObject对象的join操作来存储在list或tuple中的一组PyStringObject对象进行连接操作,这种做法只需要分配一次内存。
通过"+"操作符来对字符串进行连接 操作,会调用string_concat函数。源码如下:

static PyObject *string_concat(register PyStringObject *a, register PyObject *bb){
                    register Py_ssize_t size;
                    register PyStringObject *op;
                    if (!PyString_Check(bb)) {
                        if (PyByteArray_Check(bb))
                            return PyByteArray_Concat((PyObject *)a, bb);
                        PyErr_Format(PyExc_TypeError,
                                     "cannot concatenate 'str' and '%.200s' objects",
                                     Py_TYPE(bb)->tp_name);
                        return NULL;
                    }
                    #define b ((PyStringObject *)bb)
                    ....
                    //计算字符串连接后的长度size
                    size = Py_SIZE(a) + Py_SIZE(b);
                    
                    //创建新的PyStringObject对象,其维护的用于存储字符的内存长度为size
                    /* Inline PyObject_NewVar */
                    if (size > PY_SSIZE_T_MAX - PyStringObject_SIZE) {
                        PyErr_SetString(PyExc_OverflowError,
                                        "strings are too large to concat");
                        return NULL;
                    }
                    op = (PyStringObject *)PyObject_MALLOC(PyStringObject_SIZE + 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, Py_SIZE(a));
                    Py_MEMCPY(op->ob_sval + Py_SIZE(a), b->ob_sval, Py_SIZE(b));
                    op->ob_sval[size] = '\0';
                    return (PyObject *) op;
                #undef b
                }
View Code

  而通过join函数来连接两个字符串:

static PyObject *string_join(PyStringObject *self, PyObject *orig){
                    char *sep = PyString_AS_STRING(self);
                    //'abc'.join(list),那么self就是'abc',以下seplen = len('abc')
                    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, "");
                    
                    if (seq == NULL) {
                        return NULL;
                    }
                    //获取seq中PyStringObject对象个数,保存在seqlen中
                    seqlen = PySequence_Size(seq);
                    if (seqlen == 0) {
                        Py_DECREF(seq);
                        return PyString_FromString("");
                    }
                    if (seqlen == 1) {
                        item = PySequence_Fast_GET_ITEM(seq, 0);
                        if (PyString_CheckExact(item) || PyUnicode_CheckExact(item)) {
                            Py_INCREF(item);
                            Py_DECREF(seq);
                            return item;
                        }
                    }
                    //遍历list中每一个字符,累加获得list中所有字符串后的长度
                    for (i = 0; i < seqlen; i++) {
                        const size_t old_sz = sz;
                        //seq为python中list对象,获取第i个字符串
                        item = PySequence_Fast_GET_ITEM(seq, i);
                        if (!PyString_Check(item)){
                            PyErr_Format(PyExc_TypeError,
                                         "sequence item %zd: expected string,"
                                         " %.80s found",
                                         i, Py_TYPE(item)->tp_name);
                            Py_DECREF(seq);
                            return NULL;
                        }
                        sz += PyString_GET_SIZE(item);
                        if (i != 0)
                            sz += seplen;
                        if (sz < old_sz || sz > PY_SSIZE_T_MAX) {
                            PyErr_SetString(PyExc_OverflowError,
                                "join() result is too long for a Python string");
                            Py_DECREF(seq);
                            return NULL;
                        }
                    }

                    //创建长度为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;
                }
View Code

  执行join函数,首先统计list中一共有多少个PyStringObject对象,并统计这些PyStringObject对象所维护的字符串一共有多少个,然后再申请内存。将list中的PyStringObject对象维护的字符串都拷贝到新的内存空间。<join函数 N个字符申请一次内存,+操作符 N个字符串申请N-1次内存>

 

 

 [python源码剖析]列表对象:http://www.cnblogs.com/vipchenwei/articles/6946431.html

 

posted @ 2017-06-05 18:23  看雪。  阅读(1083)  评论(0编辑  收藏  举报