[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; }
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; } ..... }
三、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 }
而通过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; }
执行join函数,首先统计list中一共有多少个PyStringObject对象,并统计这些PyStringObject对象所维护的字符串一共有多少个,然后再申请内存。将list中的PyStringObject对象维护的字符串都拷贝到新的内存空间。<join函数 N个字符申请一次内存,+操作符 N个字符串申请N-1次内存>
[python源码剖析]列表对象:http://www.cnblogs.com/vipchenwei/articles/6946431.html