您現在的位置是:首頁 > 人文
深入 Python 直譯器原始碼,我終於搞明白了字串駐留的原理
python中pos表示什麼
英文:https://arpitbhayani。me/blogs/string-interning
作者:arpit
宣告:本翻譯是出於交流學習的目的,基於 CC BY-NC-SA 4。0 授權協議。為便於閱讀,內容略有改動。
每種程式語言為了表現出色,並且實現卓越的效能,都需要有大量編譯器級與直譯器級的最佳化。
由於字串是任何程式語言中不可或缺的一個部分,因此,如果有快速操作字串的能力,就可以迅速地提高整體的效能。
在本文中,
我們將深入研究 Python 的內部實現,並瞭解 Python 如何使用一種名為字串駐留(String Interning)的技術,實現直譯器的高效能。
本文的目的不僅在於介紹 Python 的內部知識,而且還旨在使讀者能夠輕鬆地瀏覽 Python 的原始碼;因此,本文中將有很多出自 CPython 的程式碼片段。
全文提綱如下:
1、什麼是“字串駐留”?
字串駐留是一種編譯器/直譯器的最佳化方法,它透過
快取
一般性的字串,從而節省字串處理任務的空間和時間。
這種最佳化方法不會每次都建立一個新的字串副本,而是僅為每個
適當的
不可變值保留一個字串副本,並使用指標引用之。
每個字串的唯一複製被稱為它的intern,並因此而得名 String Interning。
Python貓注:String Interning 一般被譯為“字串駐留”或“字串留用”,在某些語言中可能習慣用 String Pool(字串常量池)的概念,其實是對同一種機制的不同表述。intern 作為名詞時,是“實習生、實習醫生”的意思,在此可以理解成“駐留物、駐留值”。
查詢字串 intern 的方法可能作為公開介面公開,也可能不公開。現代程式語言如 Java、Python、PHP、Ruby、Julia 等等,都支援字串駐留,以使其編譯器和直譯器做到高效能。
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種變化,找出原因,針對性的治療
總而言之,尿液可以反映身體的健康狀況,如果生活中發現自己的尿液顏色、氣味出現以上變化的,一定要引起警惕,它很可能代謝身體的泌尿系統、腎臟、肝臟等出現了問題,要及時到醫院就診,積極治療,以免病情加重而對健康造成嚴重危害...