当前位置 主页 > 服务器问题 > win服务器问题汇总 >

    详解字符串在Python内部是如何省内存的

    栏目:win服务器问题汇总 时间:2020-02-03 17:33

    起步

    Python3 起,str 就采用了 Unicode 编码(注意这里并不是 utf8 编码,尽管 .py 文件默认编码是 utf8 )。 每个标准 Unicode 字符占用 4 个字节。这对于内存来说,无疑是一种浪费。

    Unicode 是表示了一种字符集,而为了传输方便,衍生出里如 utf8 , utf16 等编码方案来节省存储空间。Python内部存储字符串也采用了类似的形式。

    三种内部表示Unicode字符串

    为了减少内存的消耗,Python使用了三种不同单位长度来表示字符串:

    每个字符 1 个字节(Latin-1) 每个字符 2 个字节(UCS-2) 每个字符 4 个字节(UCS-4)

    源码中定义字符串结构体:

    # Include/unicodeobject.h
    typedef uint32_t Py_UCS4;
    typedef uint16_t Py_UCS2;
    typedef uint8_t Py_UCS1;
    
    # Include/cpython/unicodeobject.h
    typedef struct {
      PyCompactUnicodeObject _base;
      union {
        void *any;
        Py_UCS1 *latin1;
        Py_UCS2 *ucs2;
        Py_UCS4 *ucs4;
      } data;           /* Canonical, smallest-form Unicode buffer */
    } PyUnicodeObject;
    
    

    如果字符串中所有字符都在 ascii 码范围内,那么就可以用占用 1 个字节的 Latin-1 编码进行存储。而如果字符串中存在了需要占用两个字节(比如中文字符),那么整个字符串就将采用占用 2 个字节 UCS-2 编码进行存储。

    这点可以通过 sys.getsizeof 函数外部窥探来验证这个结论:

    如图,存储 'zh' 所需的存储空间比 'z' 多 1 个字节, h 在这里占了 1 个字节;

    存储 'z中' 所需的存储空间比 '中' 多了 2 个字节,z 在这里占了 2 个字节。

    大多数的自然语言采用 2 字节的编码就够了。但如果有一个 1G 的 ascii 文本加载到内存后,在文本中插入了一个 emoji 表情,那么字符串所需的空间将扩大到 4 倍,是不是很惊喜。

    为什么内部不采用 utf8 进行编码

    最受欢迎的 Unicode 编码方案,Python内部却不使用它,为什么?

    这里就得说下 utf8 编码带来的缺点。这种编码方案每个字符的占用字节长度是变化的,这就导致了无法按所以随机访问单个字符,例如 string[n] (使用utf8编码)则需要先统计前n个字符占用的字节长度。所以由 O(1) 变成了 O(n) ,这更无法让人接受。

    因此Python内部采用了定长的方式存储字符串。

    字符串驻留机制

    另一个节省内存的方式就是将一些短小的字符串做成池,当程序要创建字符串对象前检查池中是否有满足的字符串。在内部中,仅包含下划线(_)、字母 和 数字 的长度不高过 20 的字符串才能驻留。驻留是在代码编译期间进行的,代码中的如下会进行驻留检查:

    空字符串 '' 及所有; 变量名; 参数名; 字符串常量(代码中定义的所有字符串); 字典键; 属性名称;

    驻留机制节省大量的重复字符串内存。在内部,字符串驻留池由一个全局的 dict 维护,该字段将字符串用作键:

    void PyUnicode_InternInPlace(PyObject **p)
    {
      PyObject *s = *p;
      PyObject *t;
    
      if (s == NULL || !PyUnicode_Check(s))
        return;
    
      // 对PyUnicodeObjec进行类型和状态检查
      if (!PyUnicode_CheckExact(s))
        return;
      if (PyUnicode_CHECK_INTERNED(s))
        return;
      // 创建intern机制的dict
      if (interned == NULL) {
        interned = PyDict_New();
        if (interned == NULL) {
          PyErr_Clear(); /* Don't leave an exception */
          return;
        }
      }
    
      // 对象是否存在于inter中
      t = PyDict_SetDefault(interned, s, s);
    
      // 存在, 调整引用计数
      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_REFCNT(s) -= 2;
      _PyUnicode_STATE(s).interned = SSTATE_INTERNED_MORTAL;
    }