PyStringObject 研究分析
引言
在所有的动态语言(解释器)中,字符串对象是被频繁使用的,在Python 字符串对象中,大家都知道其强大的动态拼接重组的能力,无论是使用 '+' 还是使用 'join',甚至 'x'*int 都能生成字符串对象,当然还支持序列索引切片等,那么Python 底层到底做了那些优化,请看本章关于Str对象的源码层分析?
在本章中,我们将介绍关于PyStringObject 的一些机制和原理:
- 不可改变对象机制
- intern 机制
- 缓冲机制
- PyStringObject C对象和C方法
- 使用dis生成字节码
[图片上传失败...(image-8be25-1549099814362)]
和GCC,8086,ARM等汇编代码一样,为了方便二进制字节码向高一级的低级语言(汇编)汇编码转化,如MOV, SUB 指令,Python 也有一百多个“汇编指令”,在Int对象分析中已经介绍了 CALL_FUNCTION 这条指令,它在Python 虚拟机中就是一个 宏定义:
#define CALL_FUNCTION 131 /* #args + (#kwargs<<8) */
简单理解为在PYC文件中, 会扫描到 0x83 代表将要执行call指令类似于汇编中的CALL address, 表示调用这段地址,此时EIP->address,CPU寄存器将会执行在这段字节码之后的指令字节,调整栈帧结构,压入RET地址等等操作,详细的栈帧调用过程可以在网上参考,或者做一次缓冲区溢出的小实验加深理解。
继续回到上述的字节码,对于 a = str('Python'),通过对'Python' CALL_FUNCTION 操作,将结果 STORE_NAME 到 a 变量,此时在Python虚拟机中, a 将是一个 PyStringObject, 在int章节中谈到PyIntObject,其实它们都是 PyTypeObject 对象,我们可以在命令行中做如下判断:
assert type(int) == type(str)
不抛出异常。
不可改变对象机制
Python在heap中分配的对象分成两类:可变对象和不可变对象。所谓可变对象是指,对象的内容可变,而不可变对象是指对象内容不可变。
不可变(immutable):int、字符串(string)、float、(数值型number)、元组(tuple),字符串(str)
可变(mutable):字典型(dictionary)、列表型(list)
不可变类型特点看下面的例子:
[图片上传失败...(image-823679-1549099814362)]
- 优点是,这样可以减少重复的值对内存空间的占用。
- 缺点呢,如例1所示,我要修改这个变量绑定的值,如果内存中没用存在该值的内存块,那么必须重新开辟一块内存,把新地址与变量名绑定。而不是修改变量原来指向的内存块的值,这回给执行效率带来一定的降低。
创建PyStringObject 对象的两种方法
在 int 那一章节已经粗略的介绍过PyTypeObject, 它是一个 C中的结构体,其中封装了 HEAD 信息(包括了引用计数等信息),以及所支持的大量方法,实例的初始化 init函数信息, new初始化一个对象函数信息都包括在其中。
我们先来看看以下两个方法:
- 方法一:PyString_FromString
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 - PyStringObject_SIZE) {
//判断长度溢出错误
}
if (size == 0 && (op = nullstring) != NULL) {
/*python运行时nullstring专门指向空的字符数组,因为在生成空字符串的时候对其经行了intern操作,使得nullstring指向了这个intern-dict中的对象*/
//处理 null string
}
if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
//处理单字符,判断是否在缓冲池中
}
/* Inline PyObject_NewVar */
op = (PyStringObject *)PyObject_MALLOC(PyStringObject_SIZE + size);
if (op == NULL)
return PyErr_NoMemory();
(void)PyObject_INIT_VAR(op, &PyString_Type, size);
op->ob_shash = -1;
op->ob_sstate = 0; //设置intern flag标志为:未intern
Py_MEMCPY(op->ob_sval, str, size+1);
....
//创建新的PyStringObject对象并初始化
if (size == 0) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
nullstring = op; //空字节数组指向nullstring
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);
}
//Intern机制,加入缓冲池操作等。
}
在该C函数中,可以直观的看到上面注视的五个先后步骤:
- 判断长度溢出错误
- 处理 null string
- 处理单字符,判断是否在缓冲池中
- 创建新的PyStringObject对象并初始化
- Intern机制,加入缓冲池操作
- 方法二:PyString_FromStringAndSize
主要来看其创建PyStringObject 和 上述方法有何不同。因为1,2,3和 PyString_FromString是一样的。
PyObject *
PyString_FromStringAndSize(const char *str, Py_ssize_t size){
......
op = (PyStringObject *)PyObject_MALLOC(PyStringObject_SIZE + size);
if (op == NULL)
return PyErr_NoMemory();
(void)PyObject_INIT_VAR(op, &PyString_Type, size);
op->ob_shash = -1;
op->ob_sstate = SSTATE_NOT_INTERNED;
if (str != NULL)
Py_MEMCPY(op->ob_sval, str, size);
op->ob_sval[size] = '\0';
/* 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 && str != NULL) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
return (PyObject *) op;
}
何其相似,只是这里的一句:
op->ob_sval[size] = '\0';
让我们看到如下的细节:
在C中,字符串数组其实是以 \0 结尾做字符串结束标识的,对于PyString_FromString传入的 char 指针字符数组也必须为 '\0' 结尾,上述的 Py_MEMCPY(op->ob_sval, str, size+1); 中的 size+1 就是为了使数组多一位空间来存储 '\0'的。
缓冲机制
static PyStringObject *characters[0xff + 1];
int(base=16,x='0xff') returns 254
说明characters是一个维护着255个PyStringObject的单字符数组(静态对象)。
在Python初始化完成时, 该数组对象都指向NULL.
无论在PyString_FromString还是PyString_FromStringAndSize中,我们都可以发现两处characters:
- 处理单字符,判断是否在缓冲池中。
if (size == 1 && str != NULL &&
(op = characters[*str & UCHAR_MAX]) != NULL)
{
PyString_InternInPlace(&t)
Py_INCREF(op);
return (PyObject *)op;
}
- 当size等于1的时候,先intern再加入缓冲池中。
} else if (size == 1 && str != NULL) {
PyObject *t = (PyObject *)op;
PyString_InternInPlace(&t);
op = (PyStringObject *)t;
characters[*str & UCHAR_MAX] = op;
Py_INCREF(op);
}
什么是intern机制?
上述介绍了那么多,大量的操作和intern相关,那么intern操作是什么呢? 它有什么作用?
通过观察上述源码可知:当size为0 或者 为1 的时候, 字符都会经过intern(PyString_InternInPlace)操作 。
- 观察内置intern函数
[图片上传失败...(image-5f03ad-1549099814362)]
可以发现经过intern处理过后对象指向同一片内存地址。
在创建字符串的时候, python首先会在intern维护的PyStringObject中进行查找,如果发现存在就会将该对象的引用返回,否则就会创建,分配内存,写入intern,加入缓冲等操作。由于Python中有大量的字符串操作,所有intern机制带来了极大的性能提升。PyString_InternInPlace正是对对象进行intern操作的原生函数。
- 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 (interned == NULL) {
interned = PyDict_New();
}
t = PyDict_GetItem(interned, (PyObject *)s);
//检查创建的字符串是否则intern的字典中
if (t) {
Py_INCREF(t);
Py_DECREF(*p);
*p = t;
return;
}
//将字符串添加到intern的字典中,key-value pair均为字符串本身
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中的intern flag标志
PyString_CHECK_INTERNED(s) = 1;
//#define PyString_CHECK_INTERNED(op) (((PyStringObject *)(op))->ob_sstate)
}
- intern机制小结
- intern机制核心在于 interned这个对象,其由PyDict_New()创建一个PyDictObject(后期章节将会讲到),可以类比为C++中的 map < PyObject *, PyObject * >
- interned中的指针不能作为字符创对象的有效引用,也就是上述在对引用计数 s->ob_refcnt -= 2 的原因所在。
- 在销毁一个在interned中的对象时候,会在interned中删除指向该对象的指针。
[图片上传失败...(image-114d6b-1549099814362)]
总结
PyStringObject对象在创建的过程中会经历各种机制和条件判断最终分配内存等,这些是Python虚拟机在实例化str对象时候自动进行的操作,这里我们需要注意一下 (void)PyObject_INIT_VAR(op, &PyString_Type, size); 通过PyObject_INIT_VAR 将op 和 PyString_Type经行绑定, 这里的 PyString_Type 和我们上一章节分析的 PyInt_Type 是一个东西(PyTypeObject),我们在下一节中不再讨论 intern 和 缓存机制,来看看一个字符串为什么会具有 切割,number对象的属性等
@敬贤。 知识就是力量!
2018-06-21 23:50:17 星期四