摘要:在本文中,咱們將深刻研究 Python 的內部實現,並瞭解 Python 如何使用一種名爲字符串駐留(String Interning)的技術,實現解釋器的高性能。
每種編程語言爲了表現出色,而且實現卓越的性能,都須要有大量編譯器級與解釋器級的優化。python
因爲字符串是任何編程語言中不可或缺的一個部分,所以,若是有快速操做字符串的能力,就能夠迅速地提升總體的性能。git
在本文中,咱們將深刻研究 Python 的內部實現,並瞭解 Python 如何使用一種名爲字符串駐留(String Interning)的技術,實現解釋器的高性能。 本文的目的不只在於介紹 Python 的內部知識,並且還旨在使讀者可以輕鬆地瀏覽 Python 的源代碼;所以,本文中將有不少出自 CPython 的代碼片斷。github
全文提綱以下:正則表達式
字符串駐留是一種編譯器/解釋器的優化方法,它經過緩存通常性的字符串,從而節省字符串處理任務的空間和時間。編程
這種優化方法不會每次都建立一個新的字符串副本,而是僅爲每一個適當的不可變值保留一個字符串副本,並使用指針引用之。每一個字符串的惟一拷貝被稱爲它的intern,並所以而得名 String Interning。設計模式
String Interning 通常被譯爲「字符串駐留」或「字符串留用」,在某些語言中可能習慣用 String Pool(字符串常量池)的概念,實際上是對同一種機制的不一樣表述。intern 做爲名詞時,是「實習生、實習醫生」的意思,在此能夠理解成「駐留物、駐留值」。緩存
查找字符串 intern 的方法可能做爲公開接口公開,也可能不公開。現代編程語言如 Java、Python、PHP、Ruby、Julia 等等,都支持字符串駐留,以使其編譯器和解釋器作到高性能。編程語言
字符串駐留提高了字符串比較的速度。 若是沒有駐留,當咱們要比較兩個字符串是否相等時,它的時間複雜度將上升到 O(n),即須要檢查兩個字符串中的每一個字符,才能判斷出它們是否相等。函數
可是,若是字符串是固定的,因爲相同的字符串將使用同一個對象引用,所以只需檢查指針是否相同,就足以判斷出兩個字符串是否相等,沒必要再逐一檢查每一個字符。因爲這是一個很是廣泛的操做,所以,它被典型地實現爲指針相等性校驗,僅使用一條徹底沒有內存引用的機器指令。post
字符串駐留減小了內存佔用。 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)
在 CPython 中,字符串的引用被一個名爲interned的 Python 字典所存儲、訪問和管理。 該字典在第一次調用字符串駐留時,被延遲地初始化,並持有所有已駐留字符串對象的引用。
負責駐留字符串的核心函數是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; }
清理函數從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); }
既然瞭解了字符串駐留及清理的內部原理,咱們就能夠找出 Python 中全部會被駐留的字符串。
爲了作到這點,咱們要作的就是在 CPython 源代碼中查找PyUnicode_InternInPlace 函數的調用,並查看其附近的代碼。下面是在 Python 中關於字符串駐留的一些有趣的發現。
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; } ........ }
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; }
Python 中對象的屬性能夠經過setattr函數顯式地設置,也能夠做爲類成員的一部分而隱式地設置,或者在其數據類型中預約義。
CPython 會駐留全部這些屬性名,以便實現快速查找。 如下是函數PyObject_SetAttr的代碼片斷,該函數定義在文件object.c中,負責爲 Python 對象設置新屬性。
int PyObject_SetAttr(PyObject *v, PyObject *name, PyObject *value) { ........ PyUnicode_InternInPlace(&name); ........ }
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; } ........ }
只有編譯期的字符串會被駐留。 在解釋時或編譯時指定的字符串會被駐留,而動態建立的字符串則不會。
Python貓注:這一條規則值得展開思考,我曾經在上面踩過坑……有兩個知識點,我相信 99% 的人都不知道:字符串的 join() 方法是動態建立字符串,所以其建立的字符串不會被駐留;常量摺疊機制也發生在編譯期,所以有時候容易把它跟字符串駐留搞混淆。推薦閱讀《join()方法的神奇用處與Intern機制的軟肋》
包含 ASCII 字符和下劃線的字符串會被駐留。 在編譯期間,當對字符串字面量進行駐留時,CPython 確保僅對匹配正則表達式[a-zA-Z0-9_]*的常量進行駐留,由於它們很是貼近於 Python 的標識符。
注:關於 Python 中標識符的命名規則,在 Python2 版本只有「字母、數字和下劃線」,但在 Python 3.x 版本中,已經支持 Unicode 編碼。這部份內容推薦閱讀《醒醒!Python已經支持中文變量名啦!》
Python對象第三部分:字符串駐留(https://medium.com/@bdov_/https-medium-com-bdov-python-objects-part-iii-string-interning-625d3c7319de)
Python字符串駐留的內部原理(http://guilload.com/python-string-interning/)
Python優化機制:常量摺疊(https://mp.weixin.qq.com/s/p1Zb_linFLWwPlNyA5Ui1Q)
join()方法的神奇用處與Intern機制的軟肋(https://mp.weixin.qq.com/s/M2uHVqaHe_nyO5jT60V_6Q)
本文分享自華爲雲社區《深刻 Python 解釋器源碼,我終於搞明白了字符串駐留的原理!》,原文做者:Python貓 。