您現在的位置是:首頁 > 人文

深入 Python 直譯器原始碼,我終於搞明白了字串駐留的原理

由 CDA資料分析師 發表于 人文2022-10-27
簡介為了檢查一個字串是否被駐留,CPython 實現了一個名為PyUnicode_CHECK_INTERNED的宏,同樣是定義在 unicodeobject

python中pos表示什麼

深入 Python 直譯器原始碼,我終於搞明白了字串駐留的原理

英文:https://arpitbhayani。me/blogs/string-interning

作者:arpit

宣告:本翻譯是出於交流學習的目的,基於 CC BY-NC-SA 4。0 授權協議。為便於閱讀,內容略有改動。

每種程式語言為了表現出色,並且實現卓越的效能,都需要有大量編譯器級與直譯器級的最佳化。

由於字串是任何程式語言中不可或缺的一個部分,因此,如果有快速操作字串的能力,就可以迅速地提高整體的效能。

在本文中,

我們將深入研究 Python 的內部實現,並瞭解 Python 如何使用一種名為字串駐留(String Interning)的技術,實現直譯器的高效能。

本文的目的不僅在於介紹 Python 的內部知識,而且還旨在使讀者能夠輕鬆地瀏覽 Python 的原始碼;因此,本文中將有很多出自 CPython 的程式碼片段。

全文提綱如下:

深入 Python 直譯器原始碼,我終於搞明白了字串駐留的原理

1、什麼是“字串駐留”?

字串駐留是一種編譯器/直譯器的最佳化方法,它透過

快取

一般性的字串,從而節省字串處理任務的空間和時間。

這種最佳化方法不會每次都建立一個新的字串副本,而是僅為每個

適當的

不可變值保留一個字串副本,並使用指標引用之。

每個字串的唯一複製被稱為它的intern,並因此而得名 String Interning。

Python貓注:String Interning 一般被譯為“字串駐留”或“字串留用”,在某些語言中可能習慣用 String Pool(字串常量池)的概念,其實是對同一種機制的不同表述。intern 作為名詞時,是“實習生、實習醫生”的意思,在此可以理解成“駐留物、駐留值”。

查詢字串 intern 的方法可能作為公開介面公開,也可能不公開。現代程式語言如 Java、Python、PHP、Ruby、Julia 等等,都支援字串駐留,以使其編譯器和直譯器做到高效能。

深入 Python 直譯器原始碼,我終於搞明白了字串駐留的原理

2、為什麼要駐留字串?

字串駐留提升了字串比較的速度。

如果沒有駐留,當我們要比較兩個字串是否相等時,它的時間複雜度將上升到 O(n),即需要檢查兩個字串中的每個字元,才能判斷出它們是否相等。

但是,如果字串是固定的,由於相同的字串將使用同一個物件引用,因此只需檢查指標是否相同,就足以判斷出兩個字串是否相等,不必再逐一檢查每個字元。由於這是一個非常普遍的操作,因此,它被典型地實現為指標相等性校驗,僅使用一條完全沒有記憶體引用的機器指令。

字串駐留減少了記憶體佔用。

Python 避免記憶體中充斥多餘的字串物件,透過享元設計模式共享和重用已經定義的物件,從而最佳化記憶體佔用。

3、Python的字串駐留

像大多數其它現代程式語言一樣,Python 也使用字串駐留來提高效能。在 Python 中,我們可以使用is運算子,檢查兩個物件是否引用了同一個記憶體物件。

因此,如果兩個字串物件引用了相同的記憶體物件,則is運算子將得出True,否則False。

>>> ‘python’ is ‘python’True

我們可以使用這個特定的運算子,來判斷哪些字串是被駐留的。在 CPython 的,字串駐留是透過以下函式實現的,宣告在 unicodeobject。h 中,定義在 unicodeobject。c 中。

PyAPI_FUNC(void) PyUnicode_InternInPlace(PyObject **);

為了檢查一個字串是否被駐留,CPython 實現了一個名為PyUnicode_CHECK_INTERNED的宏,同樣是定義在 unicodeobject。h 中。

這個宏表明了 Python 在PyASCIIObject結構中維護著一個名為interned的成員變數,它的值表示相應的字串是否被駐留。

#define PyUnicode_CHECK_INTERNED(op) \

(((PyASCIIObject *)(op))->state。interned)

4、字串駐留的原理

在 CPython 中,字串的引用被一個名為

interned

的 Python 字典所儲存、訪問和管理。

該字典在第一次呼叫字串駐留時,被延遲地初始化,並持有全部已駐留字串物件的引用。

4.1 如何駐留字串?

負責駐留字串的核心函式是PyUnicode_InternInPlace,它定義在 unicodeobject。c 中,當呼叫時,它會建立一個準備容納所有駐留的字串的字典interned,然後登記入參中的物件,令其鍵和值都使用相同的物件引用。

以下函式片段顯示了 Python 實現字串駐留的過程。

void

PyUnicode_InternInPlace(PyObject **p)

{

PyObject *s = *p;

……。。。

// Lazily build the dictionary to hold interned Strings

if (interned == NULL) {

interned = PyDict_New();

if (interned == NULL) {

PyErr_Clear();

return;

}

}

PyObject *t;

// Make an entry to the interned dictionary for the

// given object

t = PyDict_SetDefault(interned, s, s);

……。。。

// The two references in interned dict (key and value) are

// not counted by refcnt。

// unicode_dealloc() and _PyUnicode_ClearInterned() take

// care of this。

Py_SET_REFCNT(s, Py_REFCNT(s) - 2);

// Set the state of the string to be INTERNED

_PyUnicode_STATE(s)。interned = SSTATE_INTERNED_MORTAL;

}

4.2 如何清理駐留的字串?

清理函式從interned字典中遍歷所有的字串,調整這些物件的引用計數,並把它們標記為NOT_INTERNED,使其被垃圾回收。一旦所有的字串都被標記為NOT_INTERNED,則interned字典會被清空並刪除。

這個清理函式就是_PyUnicode_ClearInterned,在 unicodeobject。c 中定義。

void

_PyUnicode_ClearInterned(PyThreadState *tstate)

{

……。。。

// Get all the keys to the interned dictionary

PyObject *keys = PyDict_Keys(interned);

……。。。

// Interned Unicode strings are not forcibly deallocated;

// rather, we give them their stolen references back

// and then clear and DECREF the interned dict。

for (Py_ssize_t i = 0; i < n; i++) {

PyObject *s = PyList_GET_ITEM(keys, i);

……。。。

switch (PyUnicode_CHECK_INTERNED(s)) {

case SSTATE_INTERNED_IMMORTAL:

Py_SET_REFCNT(s, Py_REFCNT(s) + 1);

break;

case SSTATE_INTERNED_MORTAL:

// Restore the two references (key and value) ignored

// by PyUnicode_InternInPlace()。

Py_SET_REFCNT(s, Py_REFCNT(s) + 2);

break;

case SSTATE_NOT_INTERNED:

/* fall through */

default:

Py_UNREACHABLE();

}

// marking the string to be NOT_INTERNED

_PyUnicode_STATE(s)。interned = SSTATE_NOT_INTERNED;

}

// decreasing the reference to the initialized and

// access keys object。

Py_DECREF(keys);

// clearing the dictionary

PyDict_Clear(interned);

// clearing the object interned

Py_CLEAR(interned);

}

5、字串駐留的實現

既然瞭解了字串駐留及清理的內部原理,我們就可以找出 Python 中所有會被駐留的字串。

為了做到這點,我們要做的就是在 CPython 原始碼中查詢

PyUnicode_InternInPlace

函式的呼叫,並檢視其附近的程式碼。下面是在 Python 中關於字串駐留的一些有趣的發現。

5.1 變數、常量與函式名

CPython 對常量(例如函式名、變數名、字串字面量等)執行字串駐留。

以下程式碼出自codeobject。c,它表明在建立新的PyCode物件時,直譯器將對所有編譯期的常量、名稱和字面量進行駐留。

PyCodeObject *

PyCode_NewWithPosOnlyArgs(int argcount, int posonlyargcount, int kwonlyargcount,

int nlocals, int stacksize, int flags,

PyObject *code, PyObject *consts, PyObject *names,

PyObject *varnames, PyObject *freevars, PyObject *cellvars,

PyObject *filename, PyObject *name, int firstlineno,

PyObject *linetable)

{

……。。

if (intern_strings(names) < 0) {

return NULL;

}

if (intern_strings(varnames) < 0) {

return NULL;

}

if (intern_strings(freevars) < 0) {

return NULL;

}

if (intern_strings(cellvars) < 0) {

return NULL;

}

if (intern_string_constants(consts, NULL) < 0) {

return NULL;

}

……。。

}

5.2 字典的鍵

CPython 還會駐留任何字典物件的字串鍵。

當在字典中插入元素時,直譯器會對該元素的鍵作字串駐留。以下程式碼出自 dictobject。c,展示了實際的行為。

有趣的地方:在PyUnicode_InternInPlace函式被呼叫處有一條註釋,它問道,我們是否真的需要對所有字典中的全部鍵進行駐留?

int

PyDict_SetItemString(PyObject *v, const char *key, PyObject *item)

{

PyObject *kv;

int err;

kv = PyUnicode_FromString(key);

if (kv == NULL)

return -1;

// Invoking String Interning on the key

PyUnicode_InternInPlace(&kv); /* XXX Should we really? */

err = PyDict_SetItem(v, kv, item);

Py_DECREF(kv);

return err;

}

5.3 任何物件的屬性

Python 中物件的屬性可以透過setattr函式顯式地設定,也可以作為類成員的一部分而隱式地設定,或者在其資料型別中預定義。

CPython 會駐留所有這些屬性名,以便實現快速查詢。

以下是函式PyObject_SetAttr的程式碼片段,該函式定義在檔案object。c中,負責為 Python 物件設定新屬性。

int

PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value)

{

……。。

PyUnicode_InternInPlace(&name);

……。。

}

5.4 顯式地駐留

Python 還支援透過

sys

模組中的

intern

函式進行顯式地字串駐留。

當使用任何字串物件呼叫此函式時,該字串物件將被駐留。以下是 sysmodule。c 檔案的程式碼片段,它展示了在

sys_intern_impl函式中的字串駐留過程。

static PyObject *

sys_intern_impl(PyObject *module, PyObject *s)

{

……。。

if (PyUnicode_CheckExact(s)) {

Py_INCREF(s);

PyUnicode_InternInPlace(&s);

return s;

}

……。。

}

6、字串駐留的其它發現

只有編譯期的字串會被駐留。

在解釋時或編譯時指定的字串會被駐留,而動態建立的字串則不會。

Python貓注:這一條規則值得展開思考,我曾經在上面踩過坑……有兩個知識點,我相信 99% 的人都不知道:字串的 join() 方法是動態建立字串,因此其建立的字串不會被駐留;

常量摺疊機制

也發生在編譯期,因此有時候容易把它跟字串駐留搞混淆。

包含 ASCII 字元和下劃線的字串會被駐留。

在編譯期間,當對字串字面量進行駐留時,CPython 確保僅對匹配正則表示式

[a-zA-Z0-9_]*

的常量進行駐留,因為它們非常貼近於 Python 的識別符號。

Python貓注:關於 Python 中識別符號的命名規則,在 Python2 版本只有“字母、數字和下劃線”,但在 Python 3。x 版本中,已經支援 Unicode 編碼。

推薦文章

  • 易通反傳尋人:家人被人騙到非法傳銷組織怎麼辦

    隨後上場的就是重頭戲,被騙人員會和家裡人聯絡說,女方的父母已經知道他們兩人的關係了,還知道了女方為了被騙人員打過胎,所以女方的父母提出了與被騙人員定下這門婚事的想法,這時就會有非法傳銷組織內安排好的人員,以女方家屬的身份透過被騙人員,聯絡到...

  • 四川這個尚未開發的秘境,當地人說可媲美九寨溝,門票還免費

    那麼其實在黃龍、九寨溝之間存在一個荒無人煙的地方,這裡方圓五十里都尚未被開放,當然這是一條不會被旅行社所推薦的旅遊路線,畢竟危險係數比較高,而且沒有配套的各種措施,比較適合那些愛冒險、闖蕩的朋友們...

  • 健康出問題時,尿液或有這7種變化,找出原因,針對性的治療

    健康出問題時,尿液或有這7種變化,找出原因,針對性的治療總而言之,尿液可以反映身體的健康狀況,如果生活中發現自己的尿液顏色、氣味出現以上變化的,一定要引起警惕,它很可能代謝身體的泌尿系統、腎臟、肝臟等出現了問題,要及時到醫院就診,積極治療,以免病情加重而對健康造成嚴重危害...