Python內(nèi)存分配,常駐內(nèi)存和測(cè)量
要精通一門(mén)語(yǔ)言,熟悉其內(nèi)容分配和使用機(jī)制很重要。對(duì)于編譯型語(yǔ)言比如C,C++,內(nèi)存的使用完全由程序員自己代碼分配和管理,所以對(duì)C,C++程序員內(nèi)存機(jī)制非常熟悉。但是對(duì)于動(dòng)態(tài)語(yǔ)言,比如Python,內(nèi)存在語(yǔ)言層自動(dòng)管理,所以程序員無(wú)需關(guān)注太多細(xì)節(jié),但是如果要想自己寫(xiě)的代碼高效可靠,則也必須了解語(yǔ)言的內(nèi)存機(jī)制。本文蟲(chóng)蟲(chóng)給大家介紹Python語(yǔ)言的內(nèi)存機(jī)制,以及如何對(duì)其內(nèi)存進(jìn)行度量。
概述
考慮以下代碼:
- import numpy as np
- cc= np.ones((1024, 1024, 1024, 3), dtype=np.uint8)
該代碼將會(huì)創(chuàng)建一個(gè)3GB字節(jié)的數(shù)組,并且都用1來(lái)填充。同學(xué)們,可能會(huì)這樣預(yù)想運(yùn)行該代碼后,進(jìn)程將會(huì)自動(dòng)分配3GB的內(nèi)存用來(lái)使用,事實(shí)是不是如此呢?
測(cè)量?jī)?nèi)存的一種方法是使用“常駐內(nèi)存”,在Python中可以使用psutil庫(kù)工具獲取方便的這些信息,檢查當(dāng)前進(jìn)程的常駐內(nèi)存:
- import psutil
- psutil.Process().memory_info().rss /(1024 * 1024)
- 3093
在該示例中,進(jìn)程使用了3093MB或3.09GB,與數(shù)組大小的無(wú)區(qū)別,和預(yù)想的一樣。
但是常駐內(nèi)存實(shí)際上沒(méi)那么簡(jiǎn)單。假設(shè)在機(jī)器上運(yùn)行一些耗內(nèi)存的任務(wù)。然后切換回解釋器,再次運(yùn)行完全相同的命令:
- psutil.Process().memory_info().rss / (1024 * 1024)
- 2903.12109375
這是怎么回事? 內(nèi)存少了200MB。
為了解釋這個(gè)現(xiàn)象,需要了解操作系統(tǒng)如何內(nèi)存管理機(jī)制。
簡(jiǎn)化模型
當(dāng)前正運(yùn)行的程序都會(huì)分配一些內(nèi)存,即從操作系統(tǒng)取回虛擬內(nèi)存中的地址。 虛擬內(nèi)存是一個(gè)特定于進(jìn)程的地址空間,本質(zhì)上是來(lái)自0至264-1,進(jìn)程可以讀取或?qū)懭胱止?jié)。
在C語(yǔ)言中,程序員可以使用malloc()或者mmap()函數(shù)進(jìn)行手動(dòng)內(nèi)存分配;而在Python中,我們只需創(chuàng)建對(duì)象,Python 解釋器將在底層自動(dòng)調(diào)用malloc()或者mmap()。然后該進(jìn)程可以讀取或?qū)懭朐撎囟ǖ刂泛瓦B續(xù)字節(jié)。
Linux下可以用ltrace工具跟蹤調(diào)用malloc(),運(yùn)行下面Python代碼:
- import numpy as np
- cc = np.ones((170_000,), dtype=np.uint8)
然后可以運(yùn)行l(wèi)trace:
- ltrace -e malloc python ones.py
- ...
- _multiarray_umath.cpython-39-x86_64-linux-gnu.so->malloc(170000) = 0x5638862a45e0
- ...
整個(gè)過(guò)程Python 創(chuàng)建一個(gè)NumPy數(shù)組。
在Python引擎NumPy調(diào)用malloc()。
這樣做的結(jié)果malloc()是內(nèi)存中的地址:0x5638862a45e0。
然后,用于實(shí)現(xiàn)NumPy的C代碼可以讀取和寫(xiě)入該地址和下一個(gè)連續(xù)的169,999 個(gè)地址,每個(gè)地址代表虛擬內(nèi)存中的一個(gè)字節(jié)。
這 170,000個(gè)字節(jié)存儲(chǔ)在哪里?
它們可以存儲(chǔ)在RAM中;這是默認(rèn)設(shè)置。
它們可以存儲(chǔ)在計(jì)算機(jī)的硬盤(pán)驅(qū)動(dòng)器或磁盤(pán)上,即swap分區(qū)交換中。
一些字節(jié)可能存儲(chǔ)在 RAM 中,一些字節(jié)可能存儲(chǔ)在交換分區(qū)中。
常駐內(nèi)存
RAM很快,而硬盤(pán)IO很慢,但RAM很貴。通常電腦硬盤(pán)驅(qū)動(dòng)器空間比RAM多得多。例如,目前主流的計(jì)算機(jī)都會(huì)有2T左右的硬盤(pán)存儲(chǔ)空間,但只會(huì)16GB的RAM。
理想情況下,程序的所有內(nèi)存都將存儲(chǔ)在內(nèi)存RAM中,但計(jì)算機(jī)上運(yùn)行的各種進(jìn)程可能分配的內(nèi)存比RAM中可用的內(nèi)存多。如果發(fā)生這種情況,操作系統(tǒng)會(huì)將一些數(shù)據(jù)從RAM移動(dòng)或“交換”到硬盤(pán)驅(qū)動(dòng)器。必要時(shí),從交換分區(qū)中獲取數(shù)據(jù),并將未積極使用的數(shù)據(jù)置換進(jìn)去。
現(xiàn)在我們準(zhǔn)備定義我們的第一個(gè)內(nèi)存使用量度:常駐內(nèi)存。常駐內(nèi)存是進(jìn)程分配的內(nèi)存中有多少常駐或存儲(chǔ)在RAM中。
在第一個(gè)示例中,首先將所有3GB的已分配數(shù)組存儲(chǔ)在RAM中。
然后,當(dāng)運(yùn)行一些任務(wù)時(shí),加載這些任務(wù)需要分配很多RAM,因此操作系統(tǒng)會(huì)將一些數(shù)據(jù)從RAM交換到磁盤(pán)交換分區(qū)。結(jié)果,Python進(jìn)程的常駐內(nèi)存下降了:所有數(shù)據(jù)仍然可以訪(fǎng)問(wèn),但其中一些已移至磁盤(pán)交換分區(qū)。
分配內(nèi)存
測(cè)量分配內(nèi)存會(huì)很有用,無(wú)論操作系統(tǒng)是將數(shù)據(jù)放在RAM中還是將其交換到磁盤(pán),總是3GB內(nèi)存,程序?qū)嶋H需要多少內(nèi)存。
在 Python 中(如果使用的是Linux 或macOS),可以使用Fil memory profiler測(cè)量分配的內(nèi)存,它專(zhuān)門(mén)測(cè)量峰值分配的內(nèi)存。對(duì)于之前的示例:
常駐內(nèi)存和分配內(nèi)存之間的權(quán)衡
常駐內(nèi)存存在一些問(wèn)題:
- 內(nèi)存的使用和測(cè)量會(huì)受到其他進(jìn)程的影響,由于其他進(jìn)程可能會(huì)爭(zhēng)搶常駐內(nèi)存導(dǎo)致使用的實(shí)際使用的RAM會(huì)變化。
- 常駐內(nèi)存的上限是可用的物理RAM,所以一旦達(dá)到上限,就永遠(yuǎn)不會(huì)真正了解程序要求多少內(nèi)存。比如主機(jī)物理內(nèi)存16GB,對(duì)需要17GB內(nèi)存的程序和需要30GB 內(nèi)存的程序,它們駐留內(nèi)存的量都將一致,都將是16GB。
- 另一方面,分配的內(nèi)存不受其他進(jìn)程的影響,并告訴程序?qū)嶋H請(qǐng)求的內(nèi)容。
當(dāng)然,常駐內(nèi)存確實(shí)比分配內(nèi)存的優(yōu)勢(shì):
- 交換的內(nèi)存很可能永遠(yuǎn)不會(huì)被使用:想象一下創(chuàng)建一個(gè)數(shù)組,忘記刪除引用,然后在程序的其余部分不再實(shí)際使用它。
- 更廣泛地說(shuō),由于駐留內(nèi)存從操作系統(tǒng)的角度衡量實(shí)際使用的內(nèi)存,因此它可以捕獲對(duì)分配的內(nèi)存跟蹤不可見(jiàn)的邊緣情況。
讓我們看一個(gè)這樣的邊緣情況的例子。
總結(jié)
到目前為止示例中,我們一直在分配充滿(mǎn)1的數(shù)組。如果測(cè)量已分配的內(nèi)存,則數(shù)組填充的內(nèi)容沒(méi)有區(qū)別:可以切換到創(chuàng)建充滿(mǎn)零的數(shù)組,并且仍然得到完全相同的結(jié)果。
但是在Linux 上,再看一個(gè)例子:
- import numpy as np
- import psutil
- arr = np.zeros((1024, 1024, 1024, 3), dtype=np.uint8)
- psutil.Process().memory_info().rss/(1024 * 1024)
- 28.5546875
這次,還是分配了一個(gè)3GB的數(shù)組,但是給數(shù)組的元素都是零。然后測(cè)量常駐內(nèi)存——數(shù)組并沒(méi)有被計(jì)算到,常駐內(nèi)存只有29M。數(shù)組占用的內(nèi)存呢?
事實(shí)證明,Linux 不會(huì)費(fèi)心將所有這些零存儲(chǔ)在RAM中。而只是在實(shí)際訪(fǎng)問(wèn)數(shù)據(jù)時(shí)向RAM添加零塊,并不會(huì)實(shí)際分配內(nèi)存。
最后,需要提及的是,我們?cè)谡f(shuō)的內(nèi)存使用模型也是理想狀態(tài)的。還沒(méi)有包括文件緩存、分配器中的內(nèi)存碎片或其他可用指標(biāo)等。
話(huà)雖如此,對(duì)于許多應(yīng)用程序來(lái)說(shuō),分配的內(nèi)存可能足以作為幫助優(yōu)化程序內(nèi)存使用的必要措施。