字符串的 intern 机制是怎么一回事?

上一篇文章我们介绍了字符串的底层结构,看到里面有一个 state 字段,该字段也是一个结构体,内部定义了很多的标志位。

如果字符串的 interned 标志位大于 0,那么虚拟机将为其开启 intern 机制。那什么是 intern 机制呢?在 Python 中,某些字符串也可以像小整数对象池里的整数一样,共享给所有变量使用,从而通过避免重复创建来降低内存使用、减少性能开销,这便是 intern 机制。

Python 的做法是在虚拟机内部维护一个全局字典,所有开启 intern 机制的字符串均会保存在这里,后续如果需要使用的话,会先尝试在全局字典中获取,从而实现避免重复创建的功能。


另外 intern 机制也分为多种。

// Include/cpython/unicode.h
#define SSTATE_NOT_INTERNED 0
#define SSTATE_INTERNED_MORTAL 1
#define SSTATE_INTERNED_IMMORTAL 2
#define SSTATE_INTERNED_IMMORTAL_STATIC 3

解释一下这几个字段:

  • SSTATE_NOT_INTERNED:字符串未开启 intern 机制;

  • SSTATE_INTERNED_MORTAL:字符串开启了 intern 机制,但它不是永久驻留的,在某些情况下可能会被回收;

  • SSTATE_INTERNED_IMMORTAL:字符串开启了 intern 机制,并且是永恒对象,会永远存活于内存中;

  • SSTATE_INTERNED_IMMORTAL_STATIC:和 SSTATE_INTERNED_IMMORTAL 类似,也是开启了 intern 机制并且不会被回收的字符串,但它表示的是程序在编译期间就已创建好的静态常量字符串;


这些字段定义了字符串在内存管理中的不同驻留状态,从未驻留短暂驻留永久驻留,帮助优化字符串的内存使用和管理。

// Objects/unicodeobject.c
void
PyUnicode_InternInPlace(PyObject **p)
{
    PyInterpreterState *interp = _PyInterpreterState_GET();
    _PyUnicode_InternInPlace(interp, p);
}

void
_PyUnicode_InternInPlace(PyInterpreterState *interp, PyObject **p)
{
    PyObject *s = *p;
    // PyUnicode_Check(s) -> isinstance(s, str)
    // PyUnicode_CheckExact(s) -> type(s) is str
    if (s == NULL || !PyUnicode_Check(s)) {
        return;
    }
    if (!PyUnicode_CheckExact(s)) {
        return;
    }
    // 执行到这儿,说明 s 一定指向字符串,那么检测它是否已经开启了 intern 机制
    // 这个函数的逻辑很简单,内部会获取 state.interned,看它是否大于 0
    if (PyUnicode_CHECK_INTERNED(s)) {
        return;
    }

    // 从全局缓存中获取,这里不用关注
    PyObject *r = (PyObject *)_Py_hashtable_get(INTERNED_STRINGS, s);
    if (r != NULL && r != s) {
        Py_SETREF(*p, Py_NewRef(r));
        return;
    }

    // 接下来查看 s 指向的字符串是否是编译期间就已经静态分配好的
    // 如果是,那么它一定也是永恒对象
    if (_PyUnicode_STATE(s).statically_allocated) {
        // 将 s 设置到 INTERNED_STRINGS 哈希表中,value 也是 s
        if (_Py_hashtable_set(INTERNED_STRINGS, s, s) == 0) {
            // 将它的 interned 标志位设置为 SSTATE_INTERNED_IMMORTAL_STATIC
            _PyUnicode_STATE(*p).interned = SSTATE_INTERNED_IMMORTAL_STATIC;
        }
        return;
    }

    // 如果字符串的内存不是编译期间静态分配的,那么获取 INTERNED_STRINGS 字典
    PyObject *interned = get_interned_dict(interp);
    // 将 s 设置到 INTERNED_STRINGS 字典中
    PyObject *t = PyDict_SetDefault(interned, s, s);
    //...
    // 标记为永恒对象
    _Py_SetImmortal(s);
    // 将 interned 标志位设置为 SSTATE_INTERNED_IMMORTAL
    _PyUnicode_STATE(*p).interned = SSTATE_INTERNED_IMMORTAL;
}

当一个字符串要开启 intern 机制时,会调用 PyUnicode_InternInPlace 函数。那么问题来了,什么样的字符串会开启 intern 机制呢?

1)如果字符串为 ASCII 字符串,并且长度不超过 4096,那么会开启 intern 机制。

>>> s1 = "a" * 4096
>>> s2 = "a" * 4096
# 会开启 intern 机制,s1 和 s2 指向同一个字符串
>>> s1 is s2
True
# 并且是永恒对象
>>> sys.getrefcount(s1)
4294967295

# 长度超过了 4096,所以不会开启 intern 机制
>>> s1 = "a" * 4097
>>> s2 = "a" * 4097
>>> s1 is s2
False
# 也不是永恒对象
>>> sys.getrefcount(s1)
2

2)如果一个字符串只有一个字符,并且码点小于 256(一个字节可以表示),那么也会开启 intern 机制。

>>> hex(128)
'0x80'
# s1 和 s2 指向同一个字符串,因为开启了 intern 机制
>>> s1 = chr(128)
>>> s2 = "\x80"
>>> s1 is s2
True
# 并且是永恒对象
>>> sys.getrefcount(s1)
4294967295

# ASCII 字符指的是码点小于 128 的字符,显然 s1 和 s2 不是 ASCII 字符串
# 虽然码点小于 256,但长度不等于 1,所以不会开启 intern 机制
>>> s1 = chr(128) + "x"
>>> s2 = chr(128) + "x"
>>> s1 is s2
False
# 不是永恒对象
>>> sys.getrefcount(s1)
2

实际上,存储单个字符这种方式有点类似于 bytes 对象的缓存池。是的,正如整数有小整数对象池、bytes 对象有字符缓存池一样,字符串也有其对应的缓存池。

当创建一个字符串时,如果字符串只有一个字符,且码点小于 256。那么会先对该字符串进行 intern 操作,再将 intern 的结果缓存到池子里。同样当再次创建字符串时,检测是不是只有一个字符,然后检查字符是不是存在于缓存池中,如果存在,直接返回。

所以 intern 机制并不是大家想的那样:先检测字符串是否已经存在,如果有,就不用创建新的,从而节省内存但其实不是这样的,事实上节省内存空间是没错的,可 Python 并不是在创建字符串的时候就通过 intern 机制实现了节省空间的目的。对于任何一个字符串,解释器总是会为它创建对应的结构体实例,但如果发现创建出来的实例在 intern 字典中已经存在了,那么再将它销毁。

最后关于 intern 机制,在 Python 里面可以通过 sys.intern 函数强制开启。

>>> s1 = "憨pi-_-||"
>>> s2 = "憨pi-_-||"
>>> s1 is s2
False
>>> 
>>> s1 = sys.intern("憨pi-_-||")
>>> s2 = sys.intern("憨pi-_-||")
>>> s1 is s2
True
>>> sys.getrefcount(s1)
4294967295
>>>

相关推荐

  • 开源日历 Cal.com 项目:自定义你的时间管理(Github项目分享)
  • 如何用 JavaScript 模拟点击事件,简单实现 x, y 坐标点击?
  • SpringBoot + RabbitMQ:轻松实现邮件大批量异步推送!
  • CCL2024·第二十三届中国计算语言大会讲习班公布
  • 会议开幕倒计时三天!CCAC 2024 主要报告介绍
  • 大模型集体失智!9.11和9.9哪个大,几乎全翻车了
  • [开源]一款MES系统基础上进行二次开发的ERP系统,高效智能的运营
  • Meta开发System 2蒸馏技术,Llama 2对话模型任务准确率接近100%
  • ECCV 2024 | 模型逆向攻击高性能新范式,人脸隐私安全问题新思考
  • 对齐全量微调!这是我看过最精彩的LoRA改进
  • 多人同时导出 Excel 干崩服务器?大佬给出的解决方案太优雅了!
  • 6个强大且流行的Python爬虫库,强烈推荐!
  • Spring Cloud Eureka快读入门Demo
  • 菊花开!!!带您深入菊花链
  • 6步!!!用 Electron开发一个记事本
  • 113K Star微软甄选!!!用这个框架开发百万人爱的VSCode
  • 月之暗面新活:Kimi浏览器插件
  • 聪明的大模型都认为9.11 大于 9.9……
  • AMD与国产AI芯势力创始人领衔!2024全球AI芯片峰会首批嘉宾公布,报名正式开启
  • 顶级AI投资人发起中国大模型群聊:十大趋势、具身智能、AI超级应用