Python內(nèi)存優(yōu)化

姓名:米芃

學(xué)號(hào):16040520018

[嵌牛導(dǎo)讀]Python內(nèi)存優(yōu)化的Profile工具,最有效的優(yōu)化方法:使用slots,在python3.6中新的dict實(shí)現(xiàn)。

[嵌牛鼻子]python Linux 內(nèi)存分配

[嵌牛提問(wèn)]Python不規(guī)范代碼,質(zhì)量較差的代碼對(duì)內(nèi)存的影響是?

[嵌牛正文]實(shí)際項(xiàng)目中,pythoner更加關(guān)注的是Python的性能問(wèn)題,之前也寫(xiě)過(guò)一篇文章介紹Python性能優(yōu)化的一些方法。而本文,關(guān)注的是Python的內(nèi)存優(yōu)化,一般說(shuō)來(lái),如果不發(fā)生內(nèi)存泄露,運(yùn)行在服務(wù)端的Python代碼不用太關(guān)心內(nèi)存,但是如果運(yùn)行在客戶端(比如移動(dòng)平臺(tái)上),那還是有優(yōu)化的必要。具體而言,本文主要針對(duì)的Cpython,而且不涉及C擴(kuò)展。

我們知道,Python使用引用技術(shù)和垃圾回收來(lái)管理內(nèi)存,底層也有各種類型的內(nèi)存池,那我們?cè)趺吹弥欢未a使用的內(nèi)存情況呢?工欲善其事必先利其器,直接看windows下的任務(wù)管理器或者linux下的top肯定是不準(zhǔn)的。

Pytracemalloc

對(duì)于基本類型,可以通過(guò)sys.getsizeof()來(lái)查看對(duì)象占用的內(nèi)存大小。以下是在64位Linux下的一些結(jié)果:

>>> import sys

>>> sys.getsizeof(1)

24

>>> sys.getsizeof([])

72

>>> sys.getsizeof(())

56

>>> sys.getsizeof({})

280

>>> sys.getsizeof(True)

24

可以看到,即使是一個(gè)int類型(1)也需要占用24個(gè)字節(jié),遠(yuǎn)遠(yuǎn)高于C語(yǔ)言中int的范圍。因?yàn)镻ython中一切都是對(duì)象,int也不例外(事實(shí)上是PyIntObject),除了真正存儲(chǔ)的數(shù)值,還需要保存引用計(jì)數(shù)信息、類型信息,更具體的可以參見(jiàn)《Python源碼剖析》。

而對(duì)于更復(fù)雜的組合類型,復(fù)雜的代碼,使用getsizeof來(lái)查看就不準(zhǔn)確了,因?yàn)樵赑ython中變量?jī)H僅指向一個(gè)對(duì)象,這個(gè)時(shí)候就需要更高級(jí)的工具,比如guppy,pysizer,pytracemalloc,objgraph。在這里重點(diǎn)介紹pytracemalloc。

在Python3.4中,已經(jīng)支持了pytracemalloc,如果使用python2.7版本,則需要對(duì)源碼打補(bǔ)丁,然后重新編譯。pytracemalloc在pep454中提出,主要有以下幾個(gè)特點(diǎn):

Traceback where an object was allocated

Statistics on allocated memory blocks per filename and per line number: total size, number and average size of allocated memory blocks

Compute the differences between two snapshots to detect memory leaks

簡(jiǎn)單來(lái)說(shuō),pytracemalloc hook住了python申請(qǐng)和釋放內(nèi)存的接口,從而能夠追蹤對(duì)象的分配和回收情況。對(duì)內(nèi)存分配的統(tǒng)計(jì)數(shù)據(jù)可以精確到每個(gè)文件、每一行代碼,也可以按照調(diào)用棧做聚合分析。而且還支持快照(snapshot)功能,比較兩個(gè)快照之間的差異可以發(fā)現(xiàn)潛在的內(nèi)存泄露。

下面通過(guò)一個(gè)例子來(lái)簡(jiǎn)單介紹pytracemalloc的用法和接口,關(guān)于更詳細(xì)用法和API,可以參考這份詳盡的文檔或者pytracemalloc的作者在pycon上的演講ppt(https://github.com/haypo/conf/blob/master/2014-Pycon-Montreal/tracemalloc.pdf)。

import tracemalloc

NUM_OF_ATTR =? 10

NUM_OF_INSTANCE = 100

class Slots(object):

? ? __slots__ = ['attr%s'%i for i in range(NUM_OF_ATTR)]

? ? def __init__(self):

? ? ? ? value_lst = (1.0, True, [], {}, ())

? ? ? ? for i in range(NUM_OF_ATTR):

? ? ? ? ? ? setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])

class NoSlots(object):

? ? def __init__(self):

? ? ? ? value_lst = (1.0, True, [], {}, ())

? ? ? ? for i in range(NUM_OF_ATTR):

? ? ? ? ? ? setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])

def generate_some_objs():

? ? lst = []

? ? for i in range(NUM_OF_INSTANCE):

? ? ? ? o = Slots() if i % 2 else NoSlots()

? ? ? ? lst.append(o)

? ? return lst

if __name__ == '__main__':

? ? tracemalloc.start(3)

? ? t = generate_some_objs()

? ? snapshot = tracemalloc.take_snapshot()

? ? top_stats = snapshot.statistics('lineno') # lineno filename traceback

? ? print(tracemalloc.get_traced_memory())

? ? for stat in top_stats[:10]:

? ? ? ? print(stat)

在上面的代碼中,用到了pytracemalloc幾個(gè)核心的API:

start(nframe: int=1)

pytracemalloc的一大好處就是可以隨時(shí)啟停,start函數(shù)即開(kāi)始追蹤內(nèi)存分配,相應(yīng)的stop會(huì)停止追蹤。start函數(shù)有一個(gè)參數(shù),nframes : 內(nèi)存分配時(shí)記錄的棧的深度,這個(gè)值越大,pytracemalloc本身消耗的內(nèi)存越多,在計(jì)算cumulative數(shù)據(jù)的時(shí)候有用。

get_traced_memory()

返回值是擁有兩個(gè)元素的tuple,第一個(gè)元素是當(dāng)前分配的內(nèi)存,第二個(gè)元素是自內(nèi)存追蹤啟動(dòng)以來(lái)的內(nèi)存峰值。

take_snapshot()

返回當(dāng)前內(nèi)存分配快照,返回值是Snapshot對(duì)象,該對(duì)象可以按照單個(gè)文件、單行、單個(gè)調(diào)用棧統(tǒng)計(jì)內(nèi)存分配情況

運(yùn)行環(huán)境:windows 64位python3.4

(62280, 62920)

test_pytracemalloc_use_py3.4.py:10: size=16.8 KiB, count=144, average=120 B

test_pytracemalloc_use_py3.4.py:17: size=16.7 KiB, count=142, average=120 B

test_pytracemalloc_use_py3.4.py:19: size=9952 B, count=100, average=100 B

test_pytracemalloc_use_py3.4.py:26: size=9792 B, count=102, average=96 B

test_pytracemalloc_use_py3.4.py:27: size=848 B, count=1, average=848 B

test_pytracemalloc_use_py3.4.py:34: size=456 B, count=1, average=456 B

test_pytracemalloc_use_py3.4.py:36: size=448 B, count=1, average=448 B

D:Python3.4libtracemalloc.py:474: size=64 B, count=1, average=64 B

如果將第36行的“l(fā)ineno“改成“filename”,那么結(jié)果如下

(62136, 62764)

test_pytracemalloc_use_py3.4.py:0: size=54.5 KiB, count=491, average=114 B

D:Python3.4libtracemalloc.py:0: size=64 B, count=1, average=64 B

有了Profile結(jié)果之后,可以看出來(lái)在哪個(gè)文件中有大量的內(nèi)存分配。與性能優(yōu)化相同,造成瓶頸的有兩種情況:?jiǎn)蝹€(gè)對(duì)象占用了大量的內(nèi)存;同時(shí)大量存在的小對(duì)象。對(duì)于前者,優(yōu)化的手段并不多,惰性初始化屬性可能有一些幫助;而對(duì)于后者,當(dāng)同樣類型的對(duì)象大量存在時(shí),可以使用slots進(jìn)行優(yōu)化。

Slots

默認(rèn)情況下,自定義的對(duì)象都使用dict來(lái)存儲(chǔ)屬性(通過(guò)obj.__dict__查看),而python中的dict大小一般比實(shí)際存儲(chǔ)的元素個(gè)數(shù)要大(以此降低hash沖突概率),因此會(huì)浪費(fèi)一定的空間。在新式類中使用__slots__,就是告訴Python虛擬機(jī),這種類型的對(duì)象只會(huì)用到這些屬性,因此虛擬機(jī)預(yù)留足夠的空間就行了,如果聲明了__slots__,那么對(duì)象就不會(huì)再有__dict__屬性。

使用slots到底能帶來(lái)多少內(nèi)存優(yōu)化呢,首先看看這篇文章,對(duì)于一個(gè)只有三個(gè)屬性的Image類,使用__slots__之后內(nèi)存從25.5G下降到16.2G,節(jié)省了9G的空間!

到底能省多少,取決于類自身有多少屬性、屬性的類型,以及同時(shí)存在多少個(gè)類的實(shí)例。下面通過(guò)一段簡(jiǎn)單代碼測(cè)試一下:

# -*- coding: utf-8 -*-

import sys

import tracemalloc

NUM_OF_ATTR =? 3 #3 # 10 # 30 #90

NUM_OF_INSTANCE = 10 # 10 # 100

class Slots(object):

? ? __slots__ = ['attr%s'%i for i in range(NUM_OF_ATTR)]

? ? def __init__(self):

? ? ? ? value_lst = (1.0, True, [], {}, ())

? ? ? ? for i in range(NUM_OF_ATTR):

? ? ? ? ? ? setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])

class NoSlots(object):

? ? def __init__(self):

? ? ? ? value_lst = (1.0, True, [], {}, ())

? ? ? ? for i in range(NUM_OF_ATTR):

? ? ? ? ? ? setattr(self, 'attr%s'%i, value_lst[i % len(value_lst)])

if __name__ == '__main__':

? ? clz = Slots if len(sys.argv) > 1 else NoSlots

? ? tracemalloc.start()

? ? objs = [clz() for i in range(NUM_OF_INSTANCE)]

? ? print(tracemalloc.get_traced_memory()[0])

上面的代碼,主要是在每個(gè)實(shí)例的屬性數(shù)目、并發(fā)存在的實(shí)例數(shù)目?jī)蓚€(gè)維度進(jìn)行測(cè)試,并沒(méi)有測(cè)試不同的屬性類型。結(jié)果如下表:

百分比為內(nèi)存優(yōu)化百分比,計(jì)算公式為(b – a) / b, 其中b為沒(méi)有使用__slots__時(shí)分配的內(nèi)存, a為使用了__slots__時(shí)分配的內(nèi)存。


摘自微信公眾號(hào)“程序員大咖”,有刪改。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容