python底层设计:String对象设计

目录

 

1.PyStringObject与PyString_Type

2.PyStringObject的创建

3.Intern机制

4.字符缓冲池

5.PyStringObject 效率问题


1.PyStringObject与PyString_Type

  在python中,PyStringObject是对字符串对象的实现,PyStringObject是一个拥有可变长度内存的对象,这一点容易理解,因为“Hi”和“Lian”创建的PyStringObject其内部需要的内存空间不一样大小。同时,PyStringObject也是一个不可变对象,当创建一个PyStringObject对象后,其内部维护的字符串不能再发生改变。

PyStringObject的定义:

typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;
    int ob_sstate;
    char ob_sval[1];

} PyStringObject;

      在PyStringObject定义我们可以看到,它的头部实际是一个PyObject_VAR_HEAD,其中有一个ob_size变量保存着对象中维护的可变长度内存的大小ob_sval是一个字符数组,但是它是作为一个字符指针指向一段内存的,这段内存保存着这个字符串对象维护的实际字符串。

     同c中的字符串一样,PyStringObject内部维护的字符串在末尾以‘\0’结尾,所以ob_sval指向的是一段长度为ob_size+1个字节的内存,且ob_sval[ob_szie] ='\0'.

    ob_shash的作用是缓存对象的hash值,这样可以避免每一次都要重新计算该字符串对象的hash值,如果PyStringObject还没被计算过hash值,那么ob_shash的初始化值为-1。计算一个字符串对象的hash值采用的算法:

static long string_hash(PyStringObject *a)
{
	register Py_ssize_t len;
	register unsigned char *p;
	register long x;

	if (a->ob_shash != -1)
		return a->ob_shash;
	len = a->ob_size;
	p = (unsigned char *) a->ob_sval;
	x = *p << 7;
	while (--len >= 0)
		x = (1000003*x) ^ *p++;
	x ^= a->ob_size;
	if (x == -1)
		x = -2;
	a->ob_shash = x;
	return x;
}

    ob_sstate变量标记该对象是否已经过intern机制处理,intern机制是这节非常重要的点。

PyStringObject的类型对象是PyString_Type

PyTypeObject PyString_Type = {
	PyObject_HEAD_INIT(&PyType_Type)
	0,
	"str",
	sizeof(PyStringObject),
	sizeof(char),
	string_repr, 				/* tp_repr */
	&string_as_number,			/* tp_as_number */
	&string_as_sequence,			/* tp_as_sequence */
	&string_as_mapping,			/* tp_as_mapping */
	(hashfunc)string_hash, 			/* tp_hash */

	string_methods,				/* tp_methods */

	&PyBaseString_Type,			/* tp_base */
    .......
	string_new,				/* tp_new */
	PyObject_Del,	                	/* tp_free */
};

     我们可以看到,tp_itemsize被设置为sizeof(char),对于python中任意变长对象,tp_itemsize这个域必须设置,它指明了由变长对象保存的元素的单位长度,tp_itemsize和ob_size共同决定了还需要额外申请的内存大小。

2.PyStringObject的创建

       python提供了两条路径从c中原生的字符串创建PyStringObject对象:

PyAPI_FUNC(PyObject *) PyString_FromStringAndSize(const char *, Py_ssize_t);
PyAPI_FUNC(PyObject *) PyString_FromString(const char *);

      我们先查看PyString_FromString的源码:

PyObject *PyString_FromString(const char *str)
{
	register size_t size;
	register PyStringObject *op;

	assert(str != NULL);
    【1】:判断字符串长度
	size = strlen(str);
	if (size > PY_SSIZE_T_MAX - sizeof(PyStringObject)) {
		PyErr_SetString(PyExc_OverflowError,
			"string is too long for a Python string");
		return NULL;
	}
    【2】:处理null string
	if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
		null_strings++;
#endif
		Py_INCREF(op);
		return (PyObject *)op;
	}
    【3】:处理单字符,从缓存池中获取
	if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
#ifdef COUNT_ALLOCS
		one_strings++;
#endif
		Py_INCREF(op);
		return (PyObject *)op;
	}
    【4】:创建新的PyStringObject对象,并初始化
	/* 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;
	Py_MEMCPY(op->ob_sval, str, size+1);
	.........
	return (PyObject *) op;
}

     在【2】中,第一次在一个空字符串基础上创建一个PyStringObject,由于nullstring指针被初始化为NULL,所以python会为这个空字符串建立一个PyStringObject对象,将这个PyStringObject通过intern机制进行共享,然后nullstring指向这个被共享的对象。如果以后python检查到需要一个空字符串创建PyStringObject对象,这是nullstring已经存在,那么直接返回nullstring引用。

    如果不是创建空字符串对象,那么接下来需要进行的动作就是申请内存,创建PyStringObject对象。在【4】处可以看出,除了申请内存,还有为字符串数组内的元素申请额外内存,然后将hash值设为-1,将intern标志设置为SSTATE_NOT_INTERNED。最后将str指向的字符串数组拷贝到PyStringObject所维护的空间中。如“Python”PyStringObject对象在内存中的状态:

python底层设计:String对象设计_第1张图片

 

3.Intern机制

    在创建PyStringObject对象过程中,我们注意到,当字符数组长度为0或者1时需要进行一个动作PyString_InternInPlace,这就是前面提到的intern机制。

PyObject *
PyString_FromString(const char *str)
{
	register PyStringObject *op;
    ......//创建PyStringObject对象

    intern共享比较短的PyStringObject对象
	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;
}

    PyStringObject对象的intern机制的目的是:对于被intern之后的对象,在python运行期间,系统中只有唯一一个与之相对应的PyStringObject对象。当判断两个PyStringObject对象是否相同时,如果他们都被intern了,那么只需要简单检查他们对应的PyObject*是否相同即可。这个机制既节省空间,又简化了PyStringObject对象的比较。

   通过下面的例子来考察一下intern机制的必要性:

>>> a = "Python"
>>> b = "Python"
>>> print(id(a),id(b))
4362549296 4362549296

    首先我们创建一个PyStringObject对象a,其字符串为“Python”,随后我们再一次为字符串“Python”创建一个PyStringObject对象。通常情况下,python会为我们重新申请内存,创建一个新的PyStringObject对象b,a和b是完全不同的对象,尽管其内部维护的字符数组完全相同。

    那就带来了一个问题,如果我们创建100000个“Python”的PyStringObject呢?显然,这样必然会浪费大量的内存。因此python引入了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】:在intern中记录检查PyStringObject对象s
	if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
		PyErr_Clear();
		return;
	}
    【3】:注意这里对引用计数的调整
	s->ob_refcnt -= 2;
    【4】:调整s中的intern状态标志
	PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}

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

  • 检查传入对象是否是一个  PyStringObject对象,intern机制只能作用在PyStringObject对象上,不会对它的派生类其作用。
  • 检查传入的PyStringObject对象是否已经被intern机制处理过了,python不会对一个PyStringObject对象进行一次以上的intern操作。

    从上面的代码中还是不能知道intern是个什么东西,其实intern指向的是PyDict_New创建的一个对象,而PyDict_New创建的是一个PyDictObject对象,也就是python中的dict,c++中的map,intern机制的关键就是在系统中有一个(key,value)映射关系的集合,集合名称叫做interned。在这个集合中,记录着被intern处理过的PyStringObject对象。

    当对一个   PyStringObject对象进行intern处理时,首先在interned这个dict中国检查是否有满足一下条件的对象b(b中维护的字符串与a相同),如果存在,那么指向a的PyObject指针指向b,而a的引用计数-1,b的引用计数+1.

    下图是如果interned集合中存在b,在对a进行intern操作,原本指向a的PyObject*指针变化情况:

python底层设计:String对象设计_第2张图片

    对于被intern机制处理过的PyStringObject对象,python采用特殊的计数机制。在将一个PyStringObject对象a的PyObject作为key和value添加到interned中,此时a的引用计数进行了两次+1,由于设计者规定interned中的a指针不能被视为有效引用,所以在代码【3】处a的计数器-2,否则删除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 */
			op->ob_refcnt = 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.");
	}
	op->ob_type->tp_free(op);
}

    看到这里了,我们会发现前面提到的python通过避免新建PyStringObject来节约内存是假的。从PyString_FromString我们可以知道,无论如何,一个合法的PyStringObject对象s是会被创建的。而intern机制是在s被创建后才其作用的,通常python在运行时创建一个临时对象temp后,基本上都会调用PyString_InternInPlace对temp进行处理,intern机制会使temp引用计数减为0,从而使temp被销毁。

 

4.字符缓冲池

  上一节讲过python为整数准备了整数对象池,python为字符类型也准备了字符缓冲池。python设计者为PyStringObject设计了一个对象池characters:

static PyStringObject *characters[UCHAR_MAX + 1];

     如果创建一个单字符PyStringObject对象,会进行一下操作:

PyObject *
PyString_FromString(const char *str)
{
	register PyStringObject *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;
}

    首先对创建的字符对象进行intern操作,再将intern的结果缓存到字符缓冲池characters中。如下图演示缓存一个字符对应的PyStringObject对象的过程:

python底层设计:String对象设计_第3张图片

  1. 创建PyStringObject对象p ;
  2. 对p进行intern操作;
  3. 将p缓存到字符缓冲池中

 

5.PyStringObject 效率问题

   python中可以通过+进行字符串拼接,但是性能及其低下,由于PyStringObject是不可变对象,这意味着在拼接时必须新建一个PyStringObject对象。这样如果连接N个PyStringObject对象,需要N-1次内存的申请工作,这无疑严重影响python的执行效率。

   字符串+拼接源码:

static PyObject *
string_concat(register PyStringObject *a, register PyObject *bb)
{
	register Py_ssize_t size;
	register PyStringObject *op;
	.....
    计算字符串连接后的长度
	size = a->ob_size + b->ob_size;
	if (a->ob_size < 0 || b->ob_size < 0 ||
	    a->ob_size > PY_SSIZE_T_MAX - b->ob_size) {
		PyErr_SetString(PyExc_OverflowError,
				"strings are too large to concat");
		return NULL;
	}
	  
	/* Inline PyObject_NewVar */
	if (size > PY_SSIZE_T_MAX - sizeof(PyStringObject)) {
		PyErr_SetString(PyExc_OverflowError,
				"strings are too large to concat");
		return NULL;
	}
    创建新的PyStringObject对象,其维护的用于存储字符的内存长度为size
	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;
    将a,b字符拷贝到新建的PyStringObject中
	Py_MEMCPY(op->ob_sval, a->ob_sval, a->ob_size);
	Py_MEMCPY(op->ob_sval + a->ob_size, b->ob_sval, b->ob_size);
	op->ob_sval[size] = '\0';
	return (PyObject *) op;
#undef b
}

   对于任意两个PyStringObject对象拼接,就会进行一次内存申请工作。而如果利用PyStringObject对象的join操作,则会进行下面的动作:

static PyObject *
string_join(PyStringObject *self, PyObject *orig)
{
	char *sep = PyString_AS_STRING(self);
	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, "");
	 .....
    【1】遍历list中每个字符串,累加获取所有字符串长度

	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;
		
	}
     创建长度为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对象维护的字符串拷贝到新开辟的内存空间中。N个PyStringObject对象拼接join只需申请一次内存,比+操作节约了N-2次操作,效率的提升非常明显。

你可能感兴趣的:(Python)