Python mmap:使用內存映射改進文件 I/O
目錄
了解計算機內存
物理內存
虛擬內存
共享內存
深入了解文件 I/O
系統調用
內存映射優化
使用 Python 的 mmap 讀取內存映射文件
性能影響
mmap 對象創建
mmap 對象作為字符串
搜索內存映射文件
作為文件的內存映射對象
使用 Python 的 mmap 編寫內存映射文件
寫入模式
搜索和替換文本
使用 Python 的 mmap 在進程之間共享數據
結論
Python之禪提供了很多智慧。一個特別有用的想法是“應該有一種——最好只有一種——明顯的方法來做到這一點。”?然而,在 Python 中有多種方法可以做大多數事情,而且通常是有充分理由的。例如,在 Python 中有多種讀取文件的方法,包括很少使用的mmap模塊。
Pythonmmap提供內存映射文件輸入和輸出 (I/O)。它允許您利用較低級別的操作系統功能來讀取文件,就好像它們是一個大字符串或數組一樣。這可以在需要大量文件 I/O 的代碼中提供顯著的性能改進。
在本教程中,您將學習:
計算機內存有哪些種類
你可以解決什么問題?mmap
如何使用內存映射更快地讀取大文件
如何在不重寫整個文件的情況下更改文件的一部分
如何使用mmap來共享信息的多個進程之間
了解計算機內存
內存映射是一種使用較低級別的操作系統 API 將文件直接加載到計算機內存中的技術。它可以顯著提高程序中的文件 I/O 性能。為了更好地了解內存映射如何提高性能,以及如何以及何時可以使用該mmap模塊來利用這些性能優勢,首先了解一下計算機內存是很有用的。
計算機內存是一個大而復雜的話題,但本教程只關注mmap有效使用該模塊所需的知識。在本教程中,術語內存是指隨機存取內存或 RAM。
有幾種類型的計算機內存:
身體的
虛擬的
共享
當您使用內存映射時,每種類型的內存都可以發揮作用,所以讓我們從高層次來回顧每一種。
物理內存
物理內存是最容易理解的內存類型,因為它通常是與您的計算機相關的營銷的一部分。(您可能還記得,當您購買計算機時,它宣傳了大約 8 GB 的 RAM。)物理內存通常位于連接到計算機主板的卡上。
物理內存是可供程序在運行時使用的易失性內存量。物理內存不應與存儲混淆,例如硬盤驅動器或固態磁盤。
虛擬內存
虛擬內存是處理內存管理的一種方式。操作系統使用虛擬內存使您看起來擁有比實際更多的內存,讓您不必擔心在任何給定時間有多少內存可用于您的程序。在幕后,您的操作系統使用部分非易失性存儲(例如固態磁盤)來模擬額外的 RAM。
為此,您的操作系統必須維護物理內存和虛擬內存之間的映射。每個操作系統都使用自己復雜的算法,使用稱為頁表的數據結構將虛擬內存地址映射到物理內存地址。
幸運的是,大多數這種復雜情況都隱藏在您的程序中。您無需了解頁表或邏輯到物理映射即可在 Python 中編寫高性能 I/O 代碼。但是,了解一點內存可以讓您更好地了解計算機和圖書館正在為您處理什么。
mmap?使用虛擬內存使您看起來好像已將一個非常大的文件加載到內存中,即使該文件的內容太大而無法放入您的物理內存中。
共享內存
共享內存是操作系統提供的另一種技術,它允許多個程序同時訪問相同的數據。共享內存是在使用并發的程序中處理數據的一種非常有效的方式。
Pythonmmap使用共享內存在多 個 Python 進程、線程和同時發生的任務之間有效地共享大量數據。
深入了解文件 I/O
既然您對不同類型的內存有了一個高層次的了解,是時候了解什么是內存映射以及它解決了什么問題。內存映射是另一種執行文件 I/O 的方法,可以提高性能和內存效率。
為了充分理解內存映射的作用,從較低級別的角度考慮常規文件 I/O 很有用。讀取文件時在幕后發生了很多事情:
通過系統調用將控制權轉移到內核 或核心操作系統代碼
與文件所在的物理磁盤交互
將數據復制到用戶空間和內核空間之間的不同緩沖區中
考慮以下代碼,它執行常規 Python 文件 I/O:
def regular_io(filename): with open(filename, mode="r", encoding="utf8") as file_obj: text = file_obj.read() print(text)
如果在運行時有足夠的可用空間,此代碼會將整個文件讀入物理內存,并將其打印到屏幕上。
這種類型的文件 I/O 可能是您在 Python 之旅的早期學習過的。代碼不是很密集或復雜。然而,在函數調用的掩護下發生的事情read()非常復雜。請記住,Python 是一種高級編程語言,因此可以對程序員隱藏很多復雜性。
系統調用
在現實中,對read()操作系統的調用標志著操作系統要做很多復雜的工作。幸運的是,操作系統提供了一種通過系統調用將每個硬件設備的特定細節從程序中抽象出來的方法。每個操作系統都會以不同的方式實現此功能,但至少read()必須執行多個系統調用才能從文件中檢索數據。
對物理硬件的所有訪問都必須在稱為內核空間的受保護環境中進行。系統調用是操作系統提供的 API,允許您的程序從用戶空間轉到內核空間,在內核空間管理物理硬件的低級細節。
在這種情況下read(),操作系統需要多次系統調用才能與物理存儲設備交互并返回數據。
同樣,您不需要牢牢掌握系統調用和計算機體系結構的細節來理解內存映射。要記住的最重要的事情是系統調用在計算上相對昂貴,因此您執行的系統調用越少,您的代碼執行的速度就越快。
除了系統調用之外,在數據完全返回到您的程序之前,對 的調用read()還涉及在多個數據緩沖區之間進行大量潛在的不必要的數據復制。
通常,這一切都發生得如此之快,以至于不會引起注意。但是所有這些層都會增加延遲并會減慢您的程序速度。這就是內存映射發揮作用的地方。
內存映射優化
避免這種開銷的一種方法是使用內存映射文件。您可以將內存映射描述為一個過程,在該過程中,讀寫操作會跳過上面提到的許多層,并將請求的數據直接映射到物理內存中。
內存映射文件 I/O 方法為了速度犧牲了內存使用量,這在傳統上稱為時空權衡。但是,內存映射不必使用比傳統方法更多的內存。操作系統非常聰明。它將根據請求延遲加載數據,類似于Python 生成器的工作方式。
此外,借助虛擬內存,您可以加載比物理內存大的文件。但是,當文件沒有足夠的物理內存時,您不會看到內存映射帶來的巨大性能改進,因為操作系統將使用較慢的物理存儲介質(如固態磁盤)來模擬它缺少的物理內存.
使用 Python 讀取內存映射文件?mmap
現在,拋開所有這些理論,您可能會問自己:“我如何使用 Pythonmmap創建內存映射文件?”
這是您之前看到的文件 I/O 代碼的內存映射等效項:
import mmap def mmap_io(filename): with open(filename, mode="r", encoding="utf8") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj: text = mmap_obj.read() print(text)
這段代碼將整個文件作為字符串讀入內存并將其打印到屏幕上,就像早期的常規文件 I/O 方法所做的那樣。
簡而言之, usingmmap與傳統的讀取文件的方式非常相似,只是有一些小的變化:
打開文件open()是不夠的。您還需要使用mmap.mmap()向操作系統發出信號,表明您希望將文件映射到 RAM 中。
您需要確保您使用的模式open()與mmap.mmap().?的默認模式open()是讀取,但默認模式mmap.mmap()是讀取和寫入。因此,您在打開文件時必須明確。
您需要使用mmap對象而不是由返回的標準文件對象來執行所有讀取和寫入操作open()。
性能影響
內存映射方法比典型的文件 I/O 稍微復雜一些,因為它需要創建另一個對象。然而,在讀取只有幾兆字節的文件時,這種微小的變化可以帶來巨大的性能優勢。這是閱讀著名小說《堂吉訶德的歷史》的原始文本的比較,大約2.4兆字節:
>>>
>>> import timeit >>> timeit.repeat( ... "regular_io(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import regular_io, filename") [0.02022400000000002, 0.01988580000000001, 0.020257300000000006] >>> timeit.repeat( ... "mmap_io(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import mmap_io, filename") [0.006156499999999981, 0.004843099999999989, 0.004868600000000001]
這測量使用常規文件 I/O 和內存映射文件 I/O 讀取整個 2.4 兆字節文件的時間量。如您所見,內存映射方法大約需要 0.005 秒,而常規方法幾乎需要 0.02 秒。在讀取較大的文件時,這種性能改進可能會更大。
注意:這些結果是使用 Windows 10 和 Python 3.8 收集的。由于內存映射非常依賴于操作系統的實現,因此您的結果可能會有所不同。
Python 的mmap文件對象提供的 API與傳統的文件對象非常相似,除了一個額外的超能力:Python 的mmap文件對象可以像字符串對象一樣切片!
mmap?對象創建
mmap對象創建過程中的一些微妙之處值得更仔細地研究:
mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ)
mmap需要一個文件描述符,它來自fileno()一個普通文件對象的方法。文件描述符是一個內部標識符,通常是一個整數,操作系統使用它來跟蹤打開的文件。
的第二個參數mmap是length=0。這是內存映射的字節長度。0是一個特殊值,表示系統應該創建一個足夠大的內存映射來保存整個文件。
該access參數告訴操作系統您將如何與映射內存進行交互。選項包括ACCESS_READ,ACCESS_WRITE,ACCESS_COPY,和ACCESS_DEFAULT。這些有點類似于modebuilt-in的參數open():
ACCESS_READ?創建只讀內存映射。
ACCESS_DEFAULT默認為可選prot參數中指定的模式,用于內存保護。
ACCESS_WRITE和ACCESS_COPY是兩種寫入模式,您將在下面了解。
文件描述符、length和access參數表示創建可在 Windows、Linux 和 macOS 等操作系統上運行的內存映射文件所需的最低限度。上面的代碼是跨平臺的,這意味著它將通過所有操作系統上的內存映射接口讀取文件,而無需知道代碼在哪個操作系統上運行。
另一個有用的參數是offset,它可以是一種節省內存的技術。這指示mmap從文件中的指定偏移量開始創建內存映射。
mmap?作為字符串的對象
如前所述,內存映射透明地將文件內容作為字符串加載到內存中。因此,一旦打開文件,您就可以執行許多與字符串相同的操作,例如切片:
import mmap def mmap_io(filename): with open(filename, mode="r", encoding="utf8") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj: print(mmap_obj[10:20])
此代碼將十個字符打印mmap_obj到屏幕上,并將這十個字符讀入物理內存。同樣,數據是惰性讀取的。
切片不會推進內部文件位置。因此,如果您要read()在切片之后調用,那么您仍然會從文件的開頭讀取。
搜索內存映射文件
除了切片之外,該mmap模塊還允許其他類似字符串的行為,例如使用find()和rfind()搜索特定文本的文件。例如,這里有兩種方法可以找到" the "文件中第一次出現的:
import mmap def regular_io_find(filename): with open(filename, mode="r", encoding="utf-8") as file_obj: text = file_obj.read() print(text.find(" the ")) def mmap_io_find(filename): with open(filename, mode="r", encoding="utf-8") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj: print(mmap_obj.find(b" the "))
這兩個函數都在文件中搜索第一次出現的" the "。它們之間的主要區別在于,第一個用于find()字符串對象,而第二個用于find()內存映射文件對象。
注意:?mmap操作字節,而不是字符串。
這是性能差異:
>>>
>>> import timeit >>> timeit.repeat( ... "regular_io_find(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import regular_io_find, filename") [0.01919180000000001, 0.01940510000000001, 0.019157700000000027] >>> timeit.repeat( ... "mmap_io_find(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import mmap_io_find, filename") [0.0009397999999999906, 0.0018005999999999855, 0.000826699999999958]
這是幾個數量級的差異!同樣,您的結果可能會因您的操作系統而異。
內存映射文件也可以直接與正則表達式一起使用。考慮以下示例,該示例查找并打印出所有五個字母的單詞:
import re import mmap def mmap_io_re(filename): five_letter_word = re.compile(rb"\b[a-zA-Z]{5}\b") with open(filename, mode="r", encoding="utf-8") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj: for word in five_letter_word.findall(mmap_obj): print(word)
此代碼讀取整個文件并打印出其中恰好包含五個字母的每個單詞。請記住,內存映射文件使用字節字符串,因此正則表達式也必須使用字節字符串。
這是使用常規文件 I/O 的等效代碼:
import re def regular_io_re(filename): five_letter_word = re.compile(r"\b[a-zA-Z]{5}\b") with open(filename, mode="r", encoding="utf-8") as file_obj: for word in five_letter_word.findall(file_obj.read()): print(word)
這段代碼也打印出文件中所有五個字符的單詞,但它使用傳統的文件 I/O 機制而不是內存映射文件。和以前一樣,兩種方法的性能不同:
>>>
>>> import timeit >>> timeit.repeat( ... "regular_io_re(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import regular_io_re, filename") [0.10474110000000003, 0.10358619999999996, 0.10347820000000002] >>> timeit.repeat( ... "mmap_io_re(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import mmap_io_re, filename") [0.0740976000000001, 0.07362639999999998, 0.07380980000000004]
內存映射方法仍然快一個數量級。
作為文件的內存映射對象
內存映射文件是部分字符串和部分文件,因此mmap,您還可以執行常見的文件操作,如seek(),tell()和readline()。這些函數的工作方式與它們的常規文件對象對應物完全一樣。
例如,以下是如何搜索文件中的特定位置,然后執行單詞搜索:
import mmap def mmap_io_find_and_seek(filename): with open(filename, mode="r", encoding="utf-8") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_READ) as mmap_obj: mmap_obj.seek(10000) mmap_obj.find(b" the ")
此代碼將尋找10000文件中的位置,然后找到第一次出現的位置" the "。
seek()?在內存映射文件上的工作方式與在常規文件上的工作方式完全相同:
def regular_io_find_and_seek(filename): with open(filename, mode="r", encoding="utf-8") as file_obj: file_obj.seek(10000) text = file_obj.read() text.find(" the ")
這兩種方法的代碼非常相似。讓我們看看它們的性能比較:
>>>
>>> import timeit >>> timeit.repeat( ... "regular_io_find_and_seek(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import regular_io_find_and_seek, filename") [0.019396099999999916, 0.01936059999999995, 0.019192100000000045] >>> timeit.repeat( ... "mmap_io_find_and_seek(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import mmap_io_find_and_seek, filename") [0.000925100000000012, 0.000788299999999964, 0.0007854999999999945]
同樣,在對代碼進行一些小的調整之后,您的內存映射方法要快得多。
用 Python 編寫內存映射文件?mmap
內存映射對于讀取文件最有用,但您也可以使用它來寫入文件。mmap除了一些不同之外,用于寫入文件的API 與常規文件 I/O 非常相似。
這是將文本寫入內存映射文件的示例:
import mmap def mmap_io_write(filename, text): with open(filename, mode="w", encoding="utf-8") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_WRITE) as mmap_obj: mmap_obj.write(text)
此代碼將文本寫入內存映射文件。但是,ValueError如果創建mmap對象時文件為空,則會引發異常。
Python 的mmap模塊不允許空文件的內存映射。這是合理的,因為從概念上講,一個空的內存映射文件只是一個內存緩沖區,因此不需要內存映射對象。
通常,內存映射用于讀或讀/寫模式。例如,以下代碼演示了如何快速讀取文件并僅修改其中的一部分:
import mmap def mmap_io_write(filename): with open(filename, mode="r+") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_WRITE) as mmap_obj: mmap_obj[10:16] = b"python" mmap_obj.flush()
此函數將打開一個已包含至少 16 個字符的文件,并將字符 10 到 15 更改為"python".
寫入的更改mmap_obj在磁盤和內存中的文件中都是可見的。Python 官方文檔建議始終調用flush()以確保將數據寫回磁盤。
寫入模式
寫操作的語義由access參數控制。寫入內存映射文件和常規文件的區別之一是access參數的選項。有兩個選項可以控制如何將數據寫入內存映射文件:
ACCESS_WRITE?指定直寫語義,這意味著數據將通過內存寫入并保留在磁盤上。
ACCESS_COPY不將更改寫入磁盤,即使flush()被調用。
換句話說,ACCESS_WRITE寫入內存和文件,而ACCESS_COPY只寫入內存而不寫入底層文件。
搜索和替換文本
內存映射文件將數據作為字節串公開,但與常規字符串相比,該字節串還有另一個重要優勢。內存映射文件數據是一串可變字節。這意味著編寫代碼來搜索和替換文件中的數據會更加直接和高效:
import mmap import os import shutil def regular_io_find_and_replace(filename): with open(filename, "r", encoding="utf-8") as orig_file_obj: with open("tmp.txt", "w", encoding="utf-8") as new_file_obj: orig_text = orig_file_obj.read() new_text = orig_text.replace(" the ", " eht ") new_file_obj.write(new_text) shutil.copyfile("tmp.txt", filename) os.remove("tmp.txt") def mmap_io_find_and_replace(filename): with open(filename, mode="r+", encoding="utf-8") as file_obj: with mmap.mmap(file_obj.fileno(), length=0, access=mmap.ACCESS_WRITE) as mmap_obj: orig_text = mmap_obj.read() new_text = orig_text.replace(b" the ", b" eht ") mmap_obj[:] = new_text mmap_obj.flush()
這兩個函數的字改" the "到" eht "指定的文件英寸?如您所見,內存映射方法大致相同,但不需要手動跟蹤額外的臨時文件以進行適當的替換。
在這種情況下,對于這個文件長度,內存映射方法實際上稍微慢一些。因此,對內存映射文件進行完整的搜索和替換可能是也可能不是最有效的方法。這可能取決于許多因素,例如文件長度、機器的 RAM 速度等。也可能存在一些操作系統緩存會導致時間偏差。如您所見,常規 IO 方法會加快每次調用的速度。
>>>
>>> import timeit >>> timeit.repeat( ... "regular_io_find_and_replace(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import regular_io_find_and_replace, filename") [0.031016973999996367, 0.019185273000005054, 0.019321329999996806] >>> timeit.repeat( ... "mmap_io_find_and_replace(filename)", ... repeat=3, ... number=1, ... setup="from __main__ import mmap_io_find_and_replace, filename") [0.026475408999999672, 0.030173652999998524, 0.029132930999999473]
在這個基本的搜索和替換場景中,內存映射會生成稍微簡潔的代碼,但并不總是能大幅提高速度。正如他們所說,“您的里程可能會有所不同。”
使用 Python 在進程之間共享數據?mmap
到目前為止,您只將內存映射文件用于磁盤上的數據。但是,您也可以創建沒有物理存儲的匿名內存映射。這可以通過-1作為文件描述符傳遞來完成:
import mmap with mmap.mmap(-1, length=100, access=mmap.ACCESS_WRITE) as mmap_obj: mmap_obj[0:100] = b"a" * 100 print(mmap_obj[0:100])
這會在 RAM 中創建一個匿名內存映射對象,其中包含100字母 的副本"a"。
匿名內存映射對象本質上是length內存中由參數指定的特定大小的緩沖區。緩沖區類似于io.StringIO或io.BytesIO來自標準庫。但是,匿名內存映射對象支持跨多個進程共享,這既io.StringIO不允許也io.BytesIO不允許。
這意味著您可以使用匿名內存映射對象在進程之間交換數據,即使這些進程具有完全獨立的內存和堆棧。這是創建匿名內存映射對象以共享可以從兩個進程寫入和讀取的數據的示例:
import mmap def sharing_with_mmap(): BUF = mmap.mmap(-1, length=100, access=mmap.ACCESS_WRITE) pid = os.fork() if pid == 0: # Child process BUF[0:100] = b"a" * 100 else: time.sleep(2) print(BUF[0:100])
使用此代碼,您可以創建一個內存映射100字節緩沖區,并允許從兩個進程讀取和寫入該緩沖區。如果您想節省內存并仍然跨多個進程共享大量數據,則此方法很有用。
使用內存映射共享內存有幾個優點:
不必在進程之間復制數據。
操作系統透明地處理內存。
數據不必在進程之間進行腌制,從而節省了 CPU 時間。
說到pickling,值得指出的mmap是,它與內置multiprocessing模塊等更高級別、功能更全的 API 不兼容。該multiprocessing模塊需要在進程之間傳遞的數據來支持pickle 協議,但mmap它不支持。
您可能想使用multiprocessing代替os.fork(),如下所示:
from multiprocessing import Process def modify(buf): buf[0:100] = b"xy" * 50 if __name__ == "__main__": BUF = mmap.mmap(-1, length=100, access=mmap.ACCESS_WRITE) BUF[0:100] = b"a" * 100 p = Process(target=modify, args=(BUF,)) p.start() p.join() print(BUF[0:100])
在這里,您嘗試創建一個新進程并將其傳遞給內存映射緩沖區。這段代碼會立即引發a,TypeError因為mmap對象不能被pickle,這是將數據傳遞給第二個進程所必需的。因此,要與內存映射共享數據,您需要堅持使用較低級別的os.fork().
如果您使用的是 Python 3.8 或更新版本,那么您可以使用新shared_memory模塊更有效地跨 Python 進程共享數據:
from multiprocessing import Process from multiprocessing import shared_memory def modify(buf_name): shm = shared_memory.SharedMemory(buf_name) shm.buf[0:50] = b"b" * 50 shm.close() if __name__ == "__main__": shm = shared_memory.SharedMemory(create=True, size=100) try: shm.buf[0:100] = b"a" * 100 proc = Process(target=modify, args=(shm.name,)) proc.start() proc.join() print(bytes(shm.buf[:100])) finally: shm.close() shm.unlink()
這個小程序創建一個100字符列表并修改另一個進程的前 50 個字符。
請注意,只有緩沖區的名稱會傳遞給第二個進程。然后第二個進程可以使用唯一名稱檢索相同的內存塊。這是shared_memory由mmap.?在幕后,該shared_memory模塊使用每個操作系統的獨特 API 為您創建命名內存映射。
現在你知道了Python 3.8新特性的一些底層實現細節以及如何mmap直接使用!
結論
內存映射是 Python 程序可通過mmap模塊使用的文件 I/O 的替代方法。內存映射使用較低級別的操作系統 API 將文件內容直接存儲在物理內存中。這種方法通常會提高 I/O 性能,因為它避免了許多昂貴的系統調用并減少了昂貴的數據緩沖區傳輸。
在本教程中,您學習了:
物理內存、虛擬內存和共享內存之間的區別是什么?
如何通過內存映射優化內存使用
如何使用 Python 的mmap模塊在代碼中實現內存映射
該mmapAPI 類似于常規文件 I/O API,因此測試起來相當簡單。在您自己的代碼中試一試,看看您的程序是否可以從內存映射提供的性能改進中受益。
Python 虛擬化
版權聲明:本文內容由網絡用戶投稿,版權歸原作者所有,本站不擁有其著作權,亦不承擔相應法律責任。如果您發現本站中有涉嫌抄襲或描述失實的內容,請聯系我們jiasou666@gmail.com 處理,核實后本網站將在24小時內刪除侵權內容。