Python源码剖析-PyStringObject对象和STR(上)

PyStringObject 研究分析

引言

在所有的动态语言(解释器)中,字符串对象是被频繁使用的,在Python 字符串对象中,大家都知道其强大的动态拼接重组的能力,无论是使用 '+' 还是使用 'join',甚至 'x'*int 都能生成字符串对象,当然还支持序列索引切片等,那么Python 底层到底做了那些优化,请看本章关于Str对象的源码层分析?
在本章中,我们将介绍关于PyStringObject 的一些机制和原理:

  1. 不可改变对象机制
  2. intern 机制
  3. 缓冲机制
  4. 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函数中,可以直观的看到上面注视的五个先后步骤:

  1. 判断长度溢出错误
  2. 处理 null string
  3. 处理单字符,判断是否在缓冲池中
  4. 创建新的PyStringObject对象并初始化
  5. 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:

  1. 处理单字符,判断是否在缓冲池中。
    if (size == 1 && str != NULL &&
        (op = characters[*str & UCHAR_MAX]) != NULL)
    {
        PyString_InternInPlace(&t)
        Py_INCREF(op);
        return (PyObject *)op;
    }
  1. 当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 星期四

你可能感兴趣的:(Python源码剖析-PyStringObject对象和STR(上))