字串intern機制 | 字串駐留 | Python原始碼
有次聊天,有人說字串駐留技術還是蠻好的。看著別人一臉認真的樣子,我一臉贊同的點點頭,現在來補一補這東西是啥。
先看看字串相關定義
PyStringObject 定義
# Include/stringobject.h
typedef struct {
PyObject_VAR_HEAD
long ob_shash;
int ob_sstate;
char ob_sval[1];
} PyStringObject;
PyObject_VAR_HEAD 中的 ob_size ,記錄變長物件的記憶體大小,ov_sval 作為字元指標指向一段記憶體,這段記憶體就是實際字串。比例 test_str 的 ob_size 則為11。
ob_shash 則是該物件的雜湊值,這在 dict 型別中是非常有用的,作為 key 值存在。
ob_sstate 則是表明該物件是否經過 intern 機制處理,簡單來說就是即值同樣的字串物件僅僅會儲存一份,放在一個字串儲蓄池中,是共用的,當然,肯定不能改變,這也決定了字串必須是不可變物件
PyStringObject 建立
# Include/stringobject.h PyAPI_FUNC(PyObject *) PyString_FromStringAndSize(const char *, Py_ssize_t); PyAPI_FUNC(PyObject *) PyString_FromString(const char *); PyAPI_FUNC(PyObject *) PyString_FromFormatV(const char*, va_list) Py_GCC_ATTRIBUTE((format(printf, 1, 0))); PyAPI_FUNC(PyObject *) PyString_FromFormat(const char*, ...) Py_GCC_ATTRIBUTE((format(printf, 1, 2)));
從定義來看,可以用很多種方式建立PyStringObject,最常用為PyString_FromString
# Objects/stringobject.c # null 以及單字串,內部使用 interned 快取了,命中直接返回 static PyObject *interned; void PyString_InternInPlace(PyObject **p) { register PyStringObject *s = (PyStringObject *)(*p); PyObject *t; if (s == NULL || !PyString_Check(s)) Py_FatalError("PyString_InternInPlace: strings only please!"); /* If it's a string subclass, we don't really know what putting it in the interned dict might do. */ if (!PyString_CheckExact(s)) return; if (PyString_CHECK_INTERNED(s)) return; if (interned == NULL) { interned = PyDict_New(); if (interned == NULL) { PyErr_Clear(); /* Don't leave an exception */ return; } } t = PyDict_GetItem(interned, (PyObject *)s); if (t) { Py_INCREF(t); Py_SETREF(*p, t); return; } if (PyDict_SetItem(interned, (PyObject *)s, (PyObject *)s) < 0) { PyErr_Clear(); return; } /* The two references in interned are not counted by refcnt. The string deallocator will take care of this */ Py_REFCNT(s) -= 2; PyString_CHECK_INTERNED(s) = SSTATE_INTERNED_MORTAL; } # 字串建立 PyObject * PyString_FromString(const char *str) { register size_t size; register PyStringObject *op; assert(str != NULL); size = strlen(str); if (size > PY_SSIZE_T_MAX - PyStringObject_SIZE) { PyErr_SetString(PyExc_OverflowError, "string is too long for a Python string"); return NULL; } if (size == 0 && (op = nullstring) != NULL) { #ifdef COUNT_ALLOCS null_strings++; #endif Py_INCREF(op); return (PyObject *)op; } if (size == 1 && (op = characters[*str & UCHAR_MAX]) != NULL) { #ifdef COUNT_ALLOCS one_strings++; #endif Py_INCREF(op); return (PyObject *)op; } /* Inline PyObject_NewVar */ op = (PyStringObject *)PyObject_MALLOC(PyStringObject_SIZE + size); if (op == NULL) return PyErr_NoMemory(); (void)PyObject_INIT_VAR(op, &PyString_Type, size); op->ob_shash = -1; op->ob_sstate = SSTATE_NOT_INTERNED; Py_MEMCPY(op->ob_sval, str, size+1); /* share short strings */ if (size == 0) { PyObject *t = (PyObject *)op; PyString_InternInPlace(&t); op = (PyStringObject *)t; nullstring = op; Py_INCREF(op); } else if (size == 1) { PyObject *t = (PyObject *)op; PyString_InternInPlace(&t); op = (PyStringObject *)t; characters[*str & UCHAR_MAX] = op; Py_INCREF(op); } return (PyObject *) op; }
總結就是:
- 判斷字串是否過長,過長,則返回 null 指標
- 判斷是否是空串,空串,則將引用
- 分配記憶體,並將字串複製到 op->ob_sval 中
PyStringObject 記憶體佈局
\ | \ | ob_size | ob_shash | ob_sstate | ob_sval | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|
REF | TYPE | 6 | -1 | 0 | P | y | t | h | o | n | \0 |
快取池過渡
In [6]: a = 'thisistest'
In [7]: b = 'thisistest'
In [8]: a is b
Out[8]: True
In [9]: id(a) == id(b)
Out[9]: True
按照上述解釋,長字串,應該是重新建立。結果提示記憶體地址都一樣,那麼說明字串駐留不僅僅是指建立這一塊,應該是一種優化方式。驗證程式碼裡存在定義,用法也存在,那我們看看定義。
什麼是“字串駐留”?
字串駐留是一種編譯器/直譯器的優化方法,它通過快取一般性的字串,從而節省字串處理任務的空間和時間。
這種優化方法不會每次都建立一個新的字串副本,而是僅為每個適當的不可變值保留一個字串副本,並使用指標引用之。每個字串的唯一拷貝被稱為它的intern,並因此而得名 String Interning。
現代程式語言如 Java、Python、PHP、Ruby、Julia 等等,都支援字串駐留,以使其編譯器和直譯器做到高效能。
為什麼要駐留字串?
字串駐留提升了字串比較的速度。 如果沒有駐留,當我們要比較兩個字串是否相等時,它的時間複雜度將上升到 O(n),即需要檢查兩個字串中的每個字元,才能判斷出它們是否相等。
但是,如果字串是固定的,由於相同的字串將使用同一個物件引用,因此只需檢查指標是否相同,就足以判斷出兩個字串是否相等,不必再逐一檢查每個字元。由於這是一個非常普遍的操作,因此,它被典型地實現為指標相等性校驗,僅使用一條完全沒有記憶體引用的機器指令。
字串駐留減少了記憶體佔用。 Python 避免記憶體中充斥多餘的字串物件,通過享元設計模式共享和重用已經定義的物件,從而優化記憶體佔用。
哪些可以駐留呢?
包含 ASCII 字元和下劃線的字串會被駐留。 在編譯期間,當對字串字面量進行駐留時,CPython 確保僅對匹配正則表示式[a-zA-Z0-9_]*的常量進行駐留,因為它們非常貼近於 Python 的識別符號。
# join不駐留
In [11]: a = 'thisistest'
In [12]: a
Out[12]: 'thisistest'
In [13]: a is "".join(a)
Out[13]: False
In [14]: a == "".join(a)
Out[14]: True
# 僅對匹配正則表示式[a-zA-Z0-9_]*的常量進行駐留
In [15]: A = 'This is test'
In [16]: B = 'This is test'
In [17]: A is B
Out[17]: False
Intern 機制的大致原理很好理解,然而影響結果的還有 CPython 直譯器的其它編譯及執行機制,字串物件受到這些機制的共同影響。實際上,只有那些“看起來像” Python 識別符號的字串才會被處理。原始碼StringObject.h的註釋中寫道:
/* … … This is generally restricted to strings that “looklike” Python identifiers, although the intern() builtin can be used to force interning of any string … … */
這些機制的相互作用,不經意間帶來了不少混亂的現象:
# 長度超過20,不被intern VS 被intern
'a' * 21 is 'aaaaaaaaaaaaaaaaaaaaa'
>>> False
'aaaaaaaaaaaaaaaaaaaaa' is 'aaaaaaaaaaaaaaaaaaaaa'
>>> True
# 長度不超過20,不被intern VS 被intern
s = 'a'
s * 5 is 'aaaaa'
>>> False
'a' * 5 is 'aaaaa'
>>> True
# join方法,不被intern VS 被intern
''.join('hi') is 'hi'
>>> False
''.join('h') is 'h'
>>> True
# 特殊符號,不被intern VS 被"intern"
'python!' is 'python!'
>>> False
a, b = 'python!', 'python!'
a is b
>>> True
參考文獻