0x01 PyStringObject
typedef struct {
PyObject_VAR_HEAD
long ob_shash;
int ob_sstate;
char ob_sval[1];
/* Invariants:
* ob_sval contains space for 'ob_size+1' elements.
* ob_sval[ob_size] == 0.
* ob_shash is the hash of the string or -1 if not computed yet.
* ob_sstate != 0 iff the string object is in stringobject.c's
* 'interned' dictionary; in this case the two references
* from 'interned' to this object are *not counted* in ob_refcnt.
*/
} PyStringObject;
-
PyStringObject
是一个可变长度内存的对象,也是一个不可变对象。即当创建了一个PyStringObject
对象以后,该对象内部维护的字符串就不能被改变了。所以PyStringObject
对象可以作为字典的Key
使用。 -
ob_size
保存着对象中维护的可变长度内存的数量;ob_sval
是一个字符的字符数组,其实它是作为一个字符指针指向一段内存的,这段内存的实际长度就是由ob_size
来维护的。这个机制是python
中所有可变长对象的实现机制。 -
PyStringObject
内部维护的字符串末尾必须以'\0'
结尾,和C语言
字符串一样。所以ob_sval
指向的是一段长度为ob_size+1
个字节的内存,而且ob_sval[ob_size] == '\0'
。 -
ob_shash
变量的作用是缓存该对象的hash值
,可以避免每一次都需要计算该对象的hash值
。默认值是-1
。这个hash
在PyDictObject
对象中发挥了很大作用。 -
ob_sstate
变量标记了该对象是否经过了intern机制
的处理。 - 预存字符串的
hash值
和intern机制
使Python虚拟机
执行效率提升了20%
。
0x02 PyString_Type
PyTypeObject PyString_Type = {
PyObject_HEAD_INIT(&PyType_Type)
0,
"str",
sizeof(PyStringObject),
sizeof(char),
string_dealloc, /* tp_dealloc */
(printfunc)string_print, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_compare */
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 */
0, /* tp_call */
string_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
0, /* tp_setattro */
&string_as_buffer, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_CHECKTYPES |
Py_TPFLAGS_BASETYPE, /* tp_flags */
string_doc, /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
(richcmpfunc)string_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
string_methods, /* tp_methods */
0, /* tp_members */
0, /* tp_getset */
&PyBaseString_Type, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
0, /* tp_init */
0, /* tp_alloc */
string_new, /* tp_new */
PyObject_Del, /* tp_free */
};
-
tp_itemsize
被设置为sizeof(char)
,即一个字节。对于Python
中所有变长对象,必须设置tp_itemsize
大小。因为知道了每一个元素的大小,才能和ob_size
配合计算出需要申请的内存大小:malloc(tp_itemsize * ob_size)
。
0x03 intern机制
void
PyString_InternInPlace(PyObject **p)
{
register PyStringObject *s = (PyStringObject *)(*p);
PyObject *t;
if (s == NULL || !PyString_Check(s))
Py_FatalError("PyString_InternInPlace: strings only please!");
/* If it's a string subclass, we don't really know what putting
it in the interned dict might do. */
if (!PyString_CheckExact(s))
return;
if (PyString_CHECK_INTERNED(s))
return;
// 创建字典对象interned,用于记录经过intern机制处理后的PyStringObject对象
if (interned == NULL) {
interned = PyDict_New();
if (interned == NULL) {
PyErr_Clear(); /* Don't leave an exception */
return;
}
}
// 检查对象s,是否已经在interned字典中了
t = PyDict_GetItem(interned, (PyObject *)s);
if (t) {
Py_INCREF(t);
Py_DECREF(*p);
*p = t;
return;
}
//对象s没有在interned字典中了,将它加入到字典中
if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) {
PyErr_Clear();
return;
}
/* The two references in interned are not counted by refcnt.
The string deallocator will take care of this */
s->ob_refcnt -= 2;
// 修改对象s的ob_sstate状态
PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}
-
PyStringObject
对象的intern机制
的目的是:对于被intern
的字符串A
,在整个python
运行期间,系统中都只有唯一的一个与字符串A
对应的PyStringObject
对象。也就是对于简单的字符串使用了intern机制
以后,可以节省很多内存空间,而且比较两个PyStringObject
对象是否相同,也只需要比较他们的PyObject *
是否相同即可,不需要进行内部字符串的比较。 - 函数内先检查对象是否是
PyStringObject
对象,intern机制
只能应用在PyStringObject
对象上,它的派生类也不能使用。 -
static PyObject *interned;
被定位为一个静态全局变量。 -
intern机制图解
- 如果
PyStringObject
对象s
没有在interned
中找到,需要将它加入到interned
字典中。如上图所示,将s
对象加入到interned
字典中时,因为字典的key
和value
都引用了s
,所以s
的引用计数默认增加了2
次,但是python
规定interned
中s
的指针不能视为对象s
的有效引用(因为如果是有效引用的话,在python
结束之前,s
的引用计数永远不可能为0
,s
永远无法删除),所以最后需要将s
的引用计数减2
:s->ob_refcnt -= 2;
。 -
intern
处理后的对象有两种状态:SSTATE_INTERNED_MORTAL
和SSTATE_INTERNED_IMMORTAL
。-
SSTATE_INTERNED_IMMORTAL
状态的对象永远不会被销毁 -
SSTATE_INTERNED_MORTAL
状态的对象如果引用计数为0
了,会被销毁
-
-
PyString_InternInPlace()
函数只能创建SSTATE_INTERNED_MORTAL
状态的对象,SSTATE_INTERNED_IMMORTAL
状态对象需要调用PyString_InternImmortal()
函数。
0x04 创建PyStringObject对象
从C中原生的字符串创建PyStringObject对象
PyObject *
PyString_FromString(const char *str)
{
register size_t size;
register PyStringObject *op;
assert(str != NULL);
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;
}
if (size == 0 && (op = nullstring) != NULL) {
#ifdef COUNT_ALLOCS
null_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
#ifdef COUNT_ALLOCS
one_strings++;
#endif
Py_INCREF(op);
return (PyObject *)op;
}
/* 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);
/* share short strings */
if (size == 0) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
nullstring = op;
Py_INCREF(op);
} else if (size == 1) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
}
- 首先检查字符串长度是否超过最大限度
PY_SSIZE_T_MAX
,这个值是平台相关的。 - 然后检查是否是空串,
python
对于空串有特殊的处理(static PyStringObject *nullstring
)。维护了一个全局空串指针对象(nullstring
),只需要第一次对空串进行创建对象,然后通过intern机制
共享,以后可以直接使用。 - 接下来判断是否是单个字符(
ASCII
范围内),Python
对于单个字符的情况,全局维护了一个
characters
指针数组,指向ASCII
中的255
个字符,第一次都会创建相应的字符对象,然后intern
共享,之后直接使用相应的对象即可。 - 最后对于字符串的情况才进行申请内存,创建对象。
ob_shash
设置为-1
,ob_sstate
设置为SSTATE_NOT_INTERNED
,字符串内容拷贝到op->ob_sval
中进行管理,字符串后面的'\0'
也需要拷贝,所以size+1
了(Py_MEMCPY(op->ob_sval, str, size+1);
)
根据字符串和给定大小来创建PyStringObject对象
- 这种创建方式和第一种一样,区别在于第一种需要传入
C
字符串(最后以'\0'
结尾),因为没有'\0'
不知道字符串从哪结束;这种方式不需要字符串以'\0'
结尾,因为这里给定了字符串的大小,可以通过大小控制字符串的范围。
0x05 字符缓冲池
static PyStringObject *characters[UCHAR_MAX + 1];
-
python
对于字符串的缓冲策略就是对单字符进行缓冲。 - 以静态全局变量来存储,默认全为空指针。
- 针对某个单字符对象时,只在第一次使用时创建对象,然后
intern
共享,然后存储到characters
指针数组中,之后相同的字符直接使用该对象。
0x06 PyStringObject效率相关问题
字符串连接
大家都知道字符串连接一般通过
"+"
操作符来连接,殊不知这是影响效率的根源。
诚然,PyStringObject
对象是一个不可变对象,连接时需要创建一个新的PyStringObject
对象,这样的话对N
个字符串进行连接,就要创建N-1
次内存申请工作,极大的影响了效率。
推荐使用PyStringObject
对象的join
操作,它只需要申请一次内存。
- 统计出需要连接的
list
中的PyStringObject
对象个数,然后遍历list
,统计出list
中的所有字符串的长度,然后根据这个长度去申请内存,然后将list
中的字符创拷贝到新申请的内存中,所以在这里只需要申请一次内存,就将N
个字符串连接起来了,而且N
越大,效率提升越明显。
欢迎关注微信公众号(coder0x00)或扫描下方二维码关注,我们将持续搜寻程序员必备基础技能包提供给大家。
网友评论