CPython-内置string类型

  内置string类型关联的对象包括:string对象、string类型对象。PyStringObject结构体用来表示string对象,PyString_Type是string类型对象(PyTypeObject类型)。

  与内置int类型不同的是:string对象是变长对象,长度取决于字符串的长度。

  与内置int类型相同的是:string对象也是不可变对象,即string对象创建后不可添加/删除字符。

 

  PyStringObject

  首先来看PyStringObject结构体:

typedef struct {
    PyObject_VAR_HEAD
    long ob_shash;        // 字符串hash值(-1表示还未计算)
    int ob_sstate;        // interned状态(0表示未被intern)
    char ob_sval[1];      // 字符数组(ob_size+1,包含结束符)
} PyStringObject;

  其中PyObject_VAR_HEAD是可变对象的基础数据部分,其中有ob_size字段表示可变对象的长度;

    ob_sval是字符数组,用来存在字符串,以'\0'结尾,所以数组大小是ob_size+1;

    ob_shash是字符串的hash值,因为字符串经常会被作为字典(dict)的键(key),不管是脚本中还是Python虚拟机中,使用一个字段来保存hash值避免每次都需要重新计算;

    ob_sstate用来保存intern状态,包括SSTATE_NOT_INTERNED、SSTATE_INTERNED_MORTAL、SSTATE_INTERNED_IMMORTAL,后面会讲到。

[Python]
>>> s1 = ''
>>> sys.getsizeof(s1)
29                       # 基础大小(PyStringObject_SIZE)
>>> s2 = 'abc'
>>> sys.getsizeof(s2)
32

 

  PyString_Type

PyTypeObject PyString_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "str",
    PyStringObject_SIZE,
    sizeof(char),
    string_dealloc,                          /* tp_dealloc */
    (printfunc)string_print,                 /* tp_print */
    (hashfunc)string_hash,                   /* tp_hash */
    string_methods,                          /* tp_methods */
    ...
};

  PyObjectType中有计算字符串hash值的string_hash函数,有兴趣的可以查看string_hash函数的定义;以及string对象特有的方法string_methods,主要包括:join、split、rsplit、lower、upper、find、index、replace等。

  对于Python对象,通常需要关注对象创建、对象销毁、对象操作几部分。

 

  string对象创建

  创建string对象需要关注string对象的内存分配、字符数组创建和赋值几个部分。可以直接看string对象创建函数:

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

    assert(str != NULL);
    size = strlen(str);
    // (1)保证字符串不超长
    if (size > PY_SSIZE_T_MAX - PyStringObject_SIZE) {
        PyErr_SetString(PyExc_OverflowError, "string is too long for a Python string");
        return NULL;
    }
    // (2)空字符串nullstring
    if (size == 0 && (op = nullstring) != NULL) {
        Py_INCREF(op);
        return (PyObject *)op;
    }
    // (3)长度为1的字符串尝试从characters中获取
    if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) {
        Py_INCREF(op);
        return (PyObject *)op;
    }

    // (4)分配PyStringObject对象内存
    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;
    // (5)将字符串复制到对象内部的字符数组中
    Py_MEMCPY(op->ob_sval, str, size+1);

    if (size == 0) {
        // (6)长度为0的字符串进行intern操作
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        nullstring = op;
        Py_INCREF(op);
    } else if (size == 1) {
        // (7)长度为1的字符串进行intern操作
        PyObject *t = (PyObject *)op;
        PyString_InternInPlace(&t);
        op = (PyStringObject *)t;
        characters[*str & UCHAR_MAX] = op;
        Py_INCREF(op);
    }
    return (PyObject *) op;
}

  (1)保证字符串不超长。PY_SSIZE_T_MAX在不同设备和系统上值不同,在我的测试设备上(Windows 10)值是2147483647B(约2GB),应该几乎不可能会超值这个最大限制。

  (2)空字符串nullstring。所有的空字符串会共享nullstring静态对象(会增加引用计数)。

  (3)长度为1的字符串尝试从characters中获取(会增加引用计数)。characters是长度为1的字符串缓冲机制,是256个长度为1的字符串对象数组(UCHAR_MAX + 1),对应于ascii字符集。

    static PyStringObject *characters[UCHAR_MAX + 1];

  (4)分配PyStringObject对象内存。为string对象分配内存,大小为基础大小(PyStringObject_SIZE) + 字符串长度(size)。

  (5)将字符串复制到对象内部的字符数组中。

  (6)长度为0的字符串进行intern操作,首次创建nullstring对象,会进行intern操作。

  (7)长度为1的字符串进行intern操作,首次创建该长度为1的字符串对象,会进行intern操作。

  intern操作实现string对象驻留机制,用于对string对象的共享。

 

  intern机制

     字符串对象驻留机制,即共享机制。通过驻留能够避免相同字符串对象的重复创建,仅通过增加引用计数来取用已存在的字符串对象,这一切是通过叫interned的字典来实现。

  比如创建100个"abcdefg"字符串,如果不使用intern机制则运行时存在100个相同的string对象,造成内存和执行效率浪费,而使用了intern机制后则只需要存有1个共享的string对象。

  先通过Python脚本来观察intern机制,然后再来看intern函数源代码。

>>> s1 = 'a'
>>> s2 = 'a'
>>> s1 is s2
True                             # 单字符必interning(PyString_FromString函数中可以看出来)
>>> s1 = 'abc'
>>> s2 = 'abc'
>>> s1 is s2
True                             # 字母字符串会interning
>>> s1 = 'abc!'
>>> s2 = 'abc!'
>>> s1 is s2
False                            # 带'!'的字符串不会interning
>>> s1 = intern('abc!')
>>> s2 = intern('abc!')
>>> s1 is s2
True                             # 显式interning
>>> 'a' * 20 is 'aaaaaaaaaaaaaaaaaaaa'
True                             # 隐式interning(窥孔优化)
>>> 'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
False                            # 未interning(长度超过20未触发窥孔优化)
>>> '01234567890123456789012345' is '01234567890123456789012345'
True                             # 隐式interning

  intern分为显式和隐式两种,显式即通过调用intern函数执行,隐式则包含不少规则:

       <1>长度为0或1的字符串必interning。

      <2>常用字符(字母、数字、下划线)组成的字符串会进行interning。含有非常用字符的字符串不会进行interning,比如'!'。

      <3>常量折叠(constant folding)是Python中的一种窥孔优化(peephole optimization)技术。

    这意味着在编译表达式"a * 20"时会被替换为‘aaaaaaaaaaaaaaaaaaaa’常量以减少运行时消耗。

    只有长度小于等于20的字符串才会发生常量折叠,假如"a * 1000000"也进行常量折叠,编译得到的字节码大小会被撑爆。

   对于常用字符的检查在all_name_chars函数中,可以看一下其源代码:

#define NAME_CHARS "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz"

/* all_name_chars(s): true if all chars in s are valid NAME_CHARS */
static int all_name_chars(PyObject *o)
{
    static char ok_name_char[256];
    static const unsigned char *name_chars = (unsigned char *)NAME_CHARS;
    const unsigned char *s, *e;
    // (1)创建常用字符判定数组,加快判断速度
    if (ok_name_char[*name_chars] == 0) {
        const unsigned char *p;
        for (p = name_chars; *p; p++)
            ok_name_char[*p] = 1;
    }
    s = (unsigned char *)PyString_AS_STRING(o);
    e = s + PyString_GET_SIZE(o);
    // (2)遍历字符串,判断其所有的字符是否都是常用字符
    while (s != e) {
        if (ok_name_char[*s++] == 0)
            return 0;
    }
    return 1;
}

  

  再回到PyString_FromString函数中调用的PyString_InternInPlace函数(实现intern机制),其源代码如下:

void PyString_InternInPlace(PyObject **p)
{
    register PyStringObject *s = (PyStringObject *)(*p);
    PyObject *t;
    // (1)只对string对象使用intern机制
    if (s == NULL || !PyString_Check(s))
        Py_FatalError("PyString_InternInPlace: strings only please!");

    // (2)string子类对象不使用intern机制
    if (!PyString_CheckExact(s))
        return;
    // (3)string对象已被interned(ob_sstate)
    if (PyString_CHECK_INTERNED(s))
        return;

    // (4)interned字典未创建,则使用PyDict_New创建
    if (interned == NULL) {
        interned = PyDict_New();
        if (interned == NULL) {
            PyErr_Clear(); /* Don't leave an exception */
            return;
        }
    }

    t = PyDict_GetItem(interned, (PyObject *)s);
    // (5)已被interned,则p指向t,t引用计数加1,*p引用计数减1
    if (t) {
        Py_INCREF(t);
        Py_SETREF(*p, t);
        return;
    }

    // (6)未被interned,则将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 */
    // (7)s对象引用计数减2
    Py_REFCNT(s) -= 2;
    PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL;
}

  (4)interned字典未创建,则使用PyDict_New创建。字典是Python中常用的数据类型(PyDictObject),是关联式容器(以键值对存储),interned字典的键和值都是string对象本身。

       (5)已被interned,则p指向t,t引用计数加1,*p引用计数减1。因为原始的string对象(s)不需要了,所以需要减少s的引用计数。

       (6)未被interned,则将s加到interned字典中。这里隐藏了一条信息:interned字典的键和值都是string对象本身,所以在添加的时候会被分别增加引用计数,即加2。

       (7)s对象引用计数减2。与6对应,需要将s引用计数减2以保证能够正确销毁s,这里还设置了string对象的ob_sstate字段(SSTATE_INTERNED_MORTAL),在string对象销毁处会讲解一下它的用处。

  注意:intern机制尽管能够通过共享来减少相同string对象的数目,但在intern过程中不能够减少string对象创建,s对象就是先创建出来再来对它进行intern操作。

 

  string对象销毁

    在string对象引用计数为0时调用PyString_Type对象中的tp_dealloc字段(string_dealloc函数)进行销毁:

static void string_dealloc(PyObject *op)
{
    switch (PyString_CHECK_INTERNED(op)) {
        // (1)未被interned的字符串对象直接销毁
        case SSTATE_NOT_INTERNED:
            break;
        // (2)已被interned的字符串对象(SSTATE_INTERNED_MORTAL),从interned字典中删除后再销毁
        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;
        // (3)已被interned的字符串对象(SSTATE_INTERNED_IMMORTAL)不可被销毁
        case SSTATE_INTERNED_IMMORTAL:
            Py_FatalError("Immortal interned string died.");

        default:
            Py_FatalError("Inconsistent interned string state.");
    }
    // (4)释放string对象内存
    Py_TYPE(op)->tp_free(op);
}

  (1)未被interned的字符串对象直接销毁。

  (2)已被interned的字符串对象(SSTATE_INTERNED_MORTAL),从interned字典中删除后再销毁。因为键和值都是字符串对象本身,所以会对引用计数减2,同时又需要在从字典中删除时不销毁(第4步来销毁),所以把op->ob_refcnt设置为3。

  (3)已被interned的字符串对象(SSTATE_INTERNED_IMMORTAL)不可被销毁。SSTATE_INTERNED_IMMORTAL状态的字符串对象的创建过程和SSTATE_INTERNED_MORTAL状态的字符串对象一样,只是在创建后修改ob_sstate状态,变为永久intern字符串对象。

  (4)释放字符串对象内存。

 

posted @ 2021-04-19 00:19  carlliu3  阅读(289)  评论(0)    收藏  举报