【Python】淺談 小整數池 + 字符串駐留 (Intern 機制)

目錄

一、緒論

二、說明

2.1 小整數池

2.2 大整數池

2.3 字符串 intern 機制


一、緒論

Python 中,其實存在許多內置的性能/效率優化機制。對不可變對象而言,最常見的有 小整數池 字符串駐留 (String Interning),而學習這些對於我們瞭解 Python 及其內存優化機制是很有幫助的。

若對不可變對象的瞭解還不夠深刻,不妨先閱讀《【Python】詳解 可變對象、不可變對象 與 深淺拷貝 》一文以加深理解。

二、說明

2.1 小整數池

避免因創建相同的值而頻繁申請和回收內存空間帶來的效率問題,Python 解釋器會在啓動時創建一個範圍爲 [-5, 256] 的 小整數池,該範圍內預定義的“小”整數對象將在全局解釋器範圍內被重複使用,而不會被垃圾回收機制回收。例如,使用 is 關鍵字比較一些測試用例的 id:

>>> x = -6
>>> y = -6
>>> x is y  # id(x)=2891176270608, id(y)=2891176270192
False
# ----------------------------------------------------------------------------
>>> x = 257
>>> y = 257
>>> x is y  # id(x)=2891176270192, id(y)=2891176270608
False
# ----------------------------------------------------------------------------
>>> x = -4
>>> y = -4
>>> x is y  # id(x)=1575903296, id(y)=1575903296
True
# ----------------------------------------------------------------------------
>>> x = 256
>>> y = 256
>>> x is y  # id(x)=1575911616, id(y)=1575911616
True

再看一例:

>>> x = 257; y = 257
>>> x is y  # id(x)=2891176270800, id(y)=2891176270800
True

爲什麼 257 不在小整數池 [-5, 256] 的範圍內,之前的寫法結果爲 False,而這樣寫的結果卻爲 True 呢?原因在於:

變量賦值書寫在同一行、同時爲兩個變量賦予相同的 int 值,解釋器 知道該對象 (257) 已生成,那麼它就會引用到同一個對象 (共享引用),具有相同的內存地址;

若分多行書寫,解釋器 不知道之前該對象 (257) 已存在,於是將 重新申請內存 存放該對象 ,導致內存區域不同。

  • 編譯型 (如 C、C++):只須編譯一次就可以把源代碼編譯成機器語言,後面的執行無須重新編譯,直接使用之前的編譯結果就可以;因此其執行的效率比較高 程序執行效率比較高,但比較依賴編譯器,因此跨平臺性差一些;
  • 解釋型 (如 Python)解釋一行就執行一行,源代碼不能直接翻譯成機器語言,而是先翻譯成中間代碼,再由解釋器對中間代碼進行解釋運行; 運行效率一般相對比較低,依賴解釋器,跨平臺性好;

總之,在交互式模式下,Python 爲節省內存空間,提高執行效率使用了小整數池。在 Python 的程序中,無論該整數處於LEGB 中的哪個位置,所有位於該範圍內的整數都將指向同一個對象,字符串同理亦復如是 (intern 機制)。

2.2 大整數池

在 IDLE 中,Python 解釋器每解釋一行代碼就執行一行,所以每次“大”整數都會被重新創建。

但在 Pycharm 中,每次運行 Python 程序都將所有代碼加載到內存中 (屬於一個整體)。出於對性能的考量,會擴大小整數池範圍,從而成爲 大整數池,即處於一個代碼塊的“大”整數將指向同一個對象。

當然,其他的字符串等不可變類型對象也都包含在內並採用相同的方式處理,如下所示:

2.3 字符串 intern 機制

字符串作爲最常用的數據類型之一,Python 爲其使用效率與性能也作了許多優化,其中一者就是 intern 機制 —— Python 解釋器提供的一種用於提高使用效率的 字符串駐留技術

簡單來說,作爲不可變對象,所有值相同的字符串理應均指向同一個對象。而實現 intern 機制即是維護和使用該 字符串儲蓄池 的過程。字符串儲蓄池採用 字典結構,在創建一個新的字符串對象時 (將先在字符串儲蓄池中查找),若該字符串不在字符串儲蓄池中,且其爲符合 Python 標識符 (由字母、數字和下劃線構成) 的字符串對象,將被存入字符串儲蓄池中,並作爲程序中與之值相同的唯一字符串對象,以便下一次獲取。用僞代碼將此過程的原理描述爲:

intern_pool = {}

def intern(s):
    if s in intern_pool:
        return intern_pool[s]
    else:
        obj = PyStringObject(s)
        intern_pool[s] = obj
        return obj

下次創建一個新的字符串對象時,若該字符串不在字符串儲蓄池中,則處理方式同上;若已存在於字符串儲蓄池中,則不會再開闢新的內存空間,而是直接返回字符串儲蓄池中已創建好的字符串對象

但注意,Python 解釋器內部對 intern 機制的使用有所考量,有些場景會自動觸發,有些情況則需要手動啓動。換言之,並非所有情況都會執行上述字符串儲蓄池的相關操作。例如:

一方面,intern 機制僅在遇到符合 Python 標識符 (由字母、數字和下劃線構成) 的字符串對象時纔會觸發。

>>> x = 'cs'  # intern 機制僅在遇到符合 Python 標識符的字符串對象時纔會觸發
>>> y = 'cs'
>>> x is y
True
# ---------------------------------------------------------------------------
>>> x = "cs_go"  # 由字母、數字和下劃線構成的符合 Python 標識符的字符串即可
>>> y = "cs_go"
>>> x is y
True
# ---------------------------------------------------------------------------
>>> x = 'cs go'  # 字符串中存在空格, 不符合 Python 標識符要求, 不會啓用 intern 機制
>>> y = 'cs go'
>>> x is y
False

若無法自動觸發 intern 機制,則可用內置 sys.intern 手動啓動。intern() 函數的作用是對字符串進行 intern 機制處理 (保存在字符串儲蓄池中) 後返回字符串對象,從而令值相同的字符串共用 (指向) 同一個對象。如此能夠節省更多內存空間,系統無需爲相同的字符串重複分配內存。例如:

>>> from sys import intern  # 導入 intern() 函數

>>> s = intern("cs go")
>>> t = intern("cs go")
>>> s is t
True

 另一方面,若某字符串長度超過 20,則 Python 解釋器將判斷該字符串並非常用字符串,於是不會放入字符串儲蓄池中,自然也不會在下次自動觸發 intern 機制。

>>> x = 'c' * 20  # 字符串長度 ≤ 20
>>> y = 'c' * 20
>>> x is y
True
# ---------------------------------------------------------------------------
>>> x = 'c' * 21  # 字符串長度 > 20, 不啓用 intern 機制
>>> y = 'c' * 21
>>> x is y
False
# ---------------------------------------------------------------------------
>>> x = 'cs' * 10  # 字符串長度 ≤ 20
>>> y = 'cs' * 10
>>> x is y
True
# ---------------------------------------------------------------------------
>>> x = 'cs' * 11  # 字符串長度 > 20, 不啓用 intern 機制
>>> y = 'cs' * 11
>>> x is y
False

但不同於 IDLE,在 Pycharm 中,只要某字符串長度 ≤ 20 即可觸發 intern 機制,即便該字符串不全由字母、數字和下劃線構成。這說明 IDLE 相對輕量、支持有限,而 Pycharm 的支持更爲豐富。

總之,intern 機制的特點是:

  • 優點:創建新字符串對象時,會先在於內存 (緩存池) 中查找是否已存在具有相同值的對象 (Python 標識符 - 只含數字、字母、下劃線的字符)。若存在,則直接引用之,以避免頻繁創建和回收內存,從而提升效率
  • 缺點在拼接等需要改動字符串時會極大地影響性能,因爲 Python 字符串作爲不可變對象,對字符串的改動 (主要是拼接) 不是原地操作 (in-place),而是會開闢新的內存空間來創建新的對象。這也是使用 + 拼接字符串的效率十分低下而不建議使用的原因。 (還不如 join() - 先計算出全部字符串的長度,再一 一拷貝,僅創建一次對象)

參考文獻:

https://docs.python.org/zh-cn/3.6/library/sys.html#sys.intern

https://www.cnblogs.com/h1227/p/12403788.html

https://www.cnblogs.com/greatfish/p/6045088.html

https://www.baidu.com/link?url=ArkSkjCQJg37jCQpbB7D0bpOmHMmpD0cXAKZ7ovbo9-msS3u8o0Ky75A3P_YpyQazWLwewNIsEZLyfv_kRrxWq&wd=&eqid=bb27779d00035856000000065ec280ba

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章