當前位置 主頁 > 服務器問題 > Linux/apache問題 > 最大化 縮小

    詳解字符串在Python內部是如何省內存的

    欄目:Linux/apache問題 時間:2020-02-07 11:43

    起步

    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;
    }
    
    
    
    下一篇:沒有了
教我怎样炒股