【通过Cpython3.9源码看看python字符串的缓存机制】

基本说明

CPython中,字符串intern机制是一种字符串对象缓存机制,用于避免创建多个相同内容的字符串对象,以减少内存使用。具体来说,如果两个字符串对象的内容相同,那么这两个字符串对象实际上会共享同一块内存空间。

CPython使用了一个哈希表(即interned字典)来实现字符串对象缓存机制。当一个字符串对象被创建时,CPython会首先检查该字符串对象是否已经被缓存,如果已经被缓存,则直接返回缓存中的字符串对象的引用;否则,将该字符串对象添加到缓存中,并返回该字符串对象的引用。当一个字符串对象不再被使用时,CPython会从缓存中删除该字符串对象,以释放其占用的内存空间。

CPython中,字符串对象的intern操作可以使用sys.intern函数或PyUnicode_InternInPlace函数来实现。其中,sys.intern函数是Python层面的函数,它接受一个字符串作为参数,并返回该字符串在字符串缓存中的引用。PyUnicode_InternInPlace函数是C语言层面的函数,它接受一个字符串对象的指针作为参数,并将该字符串对象进行intern操作。

相关源码

void
PyUnicode_InternInPlace(PyObject **p)
{
    PyObject *s = *p;
#ifdef Py_DEBUG
    assert(s != NULL);
    assert(_PyUnicode_CHECK(s));
#else
    if (s == NULL || !PyUnicode_Check(s)) {
        return;
    }
#endif

    /* If it's a subclass, we don't really know what putting
       it in the interned dict might do. */
    if (!PyUnicode_CheckExact(s)) {
        return;
    }

    if (PyUnicode_CHECK_INTERNED(s)) {
        return;
    }

#ifdef INTERNED_STRINGS
    if (interned == NULL) {
        interned = PyDict_New();
        if (interned == NULL) {
            PyErr_Clear(); /* Don't leave an exception */
            return;
        }
    }

    PyObject *t;
    t = PyDict_SetDefault(interned, s, s);

    if (t == NULL) {
        PyErr_Clear();
        return;
    }

    if (t != s) {
        Py_INCREF(t);
        Py_SETREF(*p, t);
        return;
    }

    /* The two references in interned are not counted by refcnt.
       The deallocator will take care of this */
    Py_SET_REFCNT(s, Py_REFCNT(s) - 2);
    _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
#endif
}

源码解释

该函数是Python解释器内部使用的字符串intern机制的一部分。字符串intern机制通过维护一个内部的字符串缓存,以便在程序中出现多个相同内容的字符串时可以复用同一块内存空间,从而减少内存占用。

函数PyUnicode_InternInPlace的作用是将给定的字符串对象原地(in-place)进行intern操作,即将其添加到字符串缓存中。如果字符串对象已经被添加到字符串缓存中,则不会重复添加。

接下来,我们对函数的关键代码进行逐一解释:

PyObject *s = *p;

该语句从指针p中获取字符串对象的引用,并将其存储在s变量中。

#ifdef Py_DEBUG
    assert(s != NULL);
    assert(_PyUnicode_CHECK(s));
#else
    if (s == NULL || !PyUnicode_Check(s)) {
        return;
    }
#endif

这段代码是在进行一些前置检查,如果字符串对象NULL或者不是PyUnicodeObject类型,则直接返回。

if (!PyUnicode_CheckExact(s)) {
    return;
}

这段代码是检查字符串对象是否是PyUnicodeObject类型的精确子类型。如果不是,则直接返回。

if (PyUnicode_CHECK_INTERNED(s)) {
    return;
}

该语句检查字符串对象是否已经被添加到字符串缓存中,如果已经被添加,则直接返回。

if (interned == NULL) {
    interned = PyDict_New();
    if (interned == NULL) {
        PyErr_Clear(); /* Don't leave an exception */
        return;
    }
}

该语句创建了一个名为interned的全局变量,用于存储字符串缓存。如果interned变量为NULL,则创建一个新的空字典对象,并将其赋值给interned变量。如果创建字典对象失败,则清除异常状态,并返回。

PyObject *t;
t = PyDict_SetDefault(interned, s, s);

if (t == NULL) {
    PyErr_Clear();
    return;
}

这段代码是将字符串对象s添加到interned字典中,并获取interned字典中与s相同键值的对象。如果添加成功,则t变量将指向s,否则t变量为NULL。如果添加失败,则清除异常状态并返回。

if (t != s) {
    Py_INCREF(t);
    Py_SETREF(*p, t);
    return;
}

如果t变量不等于s,则说明字典中已经存在与s相同内容的字符串对象,直接使用t代替s。具体做法是将t的引用计数加1,将指针p所指的字符串对象指向t,然后返回。

/* The two references in interned are not counted by refcnt.
   The deallocator will take care of this */
Py_SET_REFCNT(s, Py_REFCNT(s) - 2);
_PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;

这段代码是将字符串对象s的引用计数减去2,并设置其为interned状态。由于在interned字典中,字符串对象s本身的引用计数没有被计入,因此需要将其引用计数减去2,以保证在字符串对象不再被使用时能够被正确地释放。同时,将字符串对象s的状态设置为SSTATE_INTERNED_MORTAL,表示该字符串对象在interned字典中被使用时是可变的。

演示说明

下面是一个简单的示例,用于演示如何使用PyUnicode_InternInPlace函数将字符串对象进行intern操作:

import sys

# 创建两个字符串对象
a = "hello"
b = "hello"

# 输出字符串对象的引用地址
print("a: ", hex(id(a)))
print("b: ", hex(id(b)))

# 将字符串对象进行 intern 操作
a = sys.intern(a)
b = sys.intern(b)

# 输出字符串对象的引用地址
print("a: ", hex(id(a)))
print("b: ", hex(id(b)))

该示例首先创建了两个相同内容的字符串对象ab,并输出它们的引用地址。然后,通过sys.getrefcount函数获取字符串对象的引用计数,并将其作为参数传递给PyUnicode_InternInPlace函数进行intern操作。最后,输出字符串对象的引用地址,可以发现ab的引用地址相同,说明它们指向了同一块内存空间,即字符串对象被成功地缓存了。

>>> # 创建两个字符串对象
>>> a = "hello"
>>> b = "hello"
>>>
>>> # 输出字符串对象的引用地址
>>> print("a: ", hex(id(a)))
a:  0x1e22d9824b0
>>> print("b: ", hex(id(b)))
b:  0x1e22d9824b0
>>>
>>> # 将字符串对象进行 intern 操作
>>> a = sys.intern(a)
>>> b = sys.intern(b)
>>>
>>> # 输出字符串对象的引用地址
>>> print("a: ", hex(id(a)))
a:  0x1e22d9824b0
>>> print("b: ", hex(id(b)))
b:  0x1e22d9824b0

你可能感兴趣的:(cpython源码分析,python,缓存)