Pythonメモリ使用量とCPU消費-辞書またはオブジェクトの面でより効率的なものは何ですか?
背景:大量のデータをPythonにロードする必要があります。単なるフィールドコンテナであるオブジェクトを作成しました。 4Mインスタンスを作成して辞書に入れるには、約10分と約6GBのメモリが必要でした。辞書の準備ができたら、すぐにアクセスできます。
例:パフォーマンスを確認するために、同じことを行う2つの簡単なプログラムを作成しました。1つはオブジェクト、他の辞書を使用しています。
オブジェクト(実行時間〜18秒):
class Obj(object):
def __init__(self, i):
self.i = i
self.l = []
all = {}
for i in range(1000000):
all[i] = Obj(i)
辞書(実行時間〜12秒):
all = {}
for i in range(1000000):
o = {}
o['i'] = i
o['l'] = []
all[i] = o
質問:何か間違ったことをしていますか、それとも辞書はオブジェクトよりも高速ですか?実際に辞書のパフォーマンスが向上した場合、誰かがその理由を説明できますか?
__slots__
を使用してみましたか?
ドキュメント から:
デフォルトでは、古いスタイルのクラスと新しいスタイルのクラスの両方のインスタンスには、属性を格納するための辞書があります。これにより、インスタンス変数が非常に少ないオブジェクトのスペースが無駄になります。大量のインスタンスを作成すると、スペースの消費が急激になる場合があります。
デフォルトは、新しいスタイルのクラス定義で
__slots__
を定義することでオーバーライドできます。__slots__
宣言は、一連のインスタンス変数を取り、各インスタンスに十分なスペースを確保して、各変数の値を保持します。__dict__
はインスタンスごとに作成されないため、スペースが節約されます。
では、これは時間とメモリを節約しますか?
コンピューターでの3つのアプローチの比較:
test_slots.py:
class Obj(object):
__slots__ = ('i', 'l')
def __init__(self, i):
self.i = i
self.l = []
all = {}
for i in range(1000000):
all[i] = Obj(i)
test_obj.py:
class Obj(object):
def __init__(self, i):
self.i = i
self.l = []
all = {}
for i in range(1000000):
all[i] = Obj(i)
test_dict.py:
all = {}
for i in range(1000000):
o = {}
o['i'] = i
o['l'] = []
all[i] = o
test_namedtuple.py(2.6でサポート):
import collections
Obj = collections.namedtuple('Obj', 'i l')
all = {}
for i in range(1000000):
all[i] = Obj(i, [])
ベンチマークを実行(CPython 2.5を使用):
$ lshw | grep product | head -n 1
product: Intel(R) Pentium(R) M processor 1.60GHz
$ python --version
Python 2.5
$ time python test_obj.py && time python test_dict.py && time python test_slots.py
real 0m27.398s (using 'normal' object)
real 0m16.747s (using __dict__)
real 0m11.777s (using __slots__)
名前付きタプルテストを含むCPython 2.6.2の使用:
$ python --version
Python 2.6.2
$ time python test_obj.py && time python test_dict.py && time python test_slots.py && time python test_namedtuple.py
real 0m27.197s (using 'normal' object)
real 0m17.657s (using __dict__)
real 0m12.249s (using __slots__)
real 0m12.262s (using namedtuple)
そのため、__slots__
の使用はパフォーマンスの最適化です。名前付きタプルを使用すると、__slots__
と同様のパフォーマンスが得られます。
オブジェクトの属性アクセスは、舞台裏で辞書アクセスを使用するため、属性アクセスを使用すると、余分なオーバーヘッドが追加されます。さらに、オブジェクトの場合は、たとえば次のような追加のオーバーヘッドが発生します。追加のメモリ割り当てとコード実行(例:__init__
メソッド)。
コードで、o
がObj
インスタンスである場合、o.attr
はo.__dict__['attr']
と同等ですが、少しのオーバーヘッドが追加されます。
namedtuple の使用を検討しましたか? ( python 2.4/2.5 のリンク))
これは、タプルのパフォーマンスとクラスの利便性を提供する構造化データを表す新しい標準的な方法です。
辞書と比べて唯一の欠点は、(タプルのように)作成後に属性を変更できないことです。
python 3.6.1の@hughdbrown回答のコピーを次に示します。カウントを5倍に増やし、各実行の最後にpythonプロセスのメモリフットプリントをテストするためのコードを追加しました。
ダウンボッターがそれに取り組む前に、オブジェクトのサイズをカウントするこの方法は正確ではないことに注意してください。
from datetime import datetime
import os
import psutil
process = psutil.Process(os.getpid())
ITER_COUNT = 1000 * 1000 * 5
RESULT=None
def makeL(i):
# Use this line to negate the effect of the strings on the test
# return "Python is smart and will only create one string with this line"
# Use this if you want to see the difference with 5 million unique strings
return "This is a sample string %s" % i
def timeit(method):
def timed(*args, **kw):
global RESULT
s = datetime.now()
RESULT = method(*args, **kw)
e = datetime.now()
sizeMb = process.memory_info().rss / 1024 / 1024
sizeMbStr = "{0:,}".format(round(sizeMb, 2))
print('Time Taken = %s, \t%s, \tSize = %s' % (e - s, method.__name__, sizeMbStr))
return timed
class Obj(object):
def __init__(self, i):
self.i = i
self.l = makeL(i)
class SlotObj(object):
__slots__ = ('i', 'l')
def __init__(self, i):
self.i = i
self.l = makeL(i)
from collections import namedtuple
NT = namedtuple("NT", ["i", 'l'])
@timeit
def profile_dict_of_nt():
return [NT(i=i, l=makeL(i)) for i in range(ITER_COUNT)]
@timeit
def profile_list_of_nt():
return dict((i, NT(i=i, l=makeL(i))) for i in range(ITER_COUNT))
@timeit
def profile_dict_of_dict():
return dict((i, {'i': i, 'l': makeL(i)}) for i in range(ITER_COUNT))
@timeit
def profile_list_of_dict():
return [{'i': i, 'l': makeL(i)} for i in range(ITER_COUNT)]
@timeit
def profile_dict_of_obj():
return dict((i, Obj(i)) for i in range(ITER_COUNT))
@timeit
def profile_list_of_obj():
return [Obj(i) for i in range(ITER_COUNT)]
@timeit
def profile_dict_of_slot():
return dict((i, SlotObj(i)) for i in range(ITER_COUNT))
@timeit
def profile_list_of_slot():
return [SlotObj(i) for i in range(ITER_COUNT)]
profile_dict_of_nt()
profile_list_of_nt()
profile_dict_of_dict()
profile_list_of_dict()
profile_dict_of_obj()
profile_list_of_obj()
profile_dict_of_slot()
profile_list_of_slot()
そして、これらは私の結果です
Time Taken = 0:00:07.018720, provile_dict_of_nt, Size = 951.83
Time Taken = 0:00:07.716197, provile_list_of_nt, Size = 1,084.75
Time Taken = 0:00:03.237139, profile_dict_of_dict, Size = 1,926.29
Time Taken = 0:00:02.770469, profile_list_of_dict, Size = 1,778.58
Time Taken = 0:00:07.961045, profile_dict_of_obj, Size = 1,537.64
Time Taken = 0:00:05.899573, profile_list_of_obj, Size = 1,458.05
Time Taken = 0:00:06.567684, profile_dict_of_slot, Size = 1,035.65
Time Taken = 0:00:04.925101, profile_list_of_slot, Size = 887.49
私の結論は:
from datetime import datetime
ITER_COUNT = 1000 * 1000
def timeit(method):
def timed(*args, **kw):
s = datetime.now()
result = method(*args, **kw)
e = datetime.now()
print method.__name__, '(%r, %r)' % (args, kw), e - s
return result
return timed
class Obj(object):
def __init__(self, i):
self.i = i
self.l = []
class SlotObj(object):
__slots__ = ('i', 'l')
def __init__(self, i):
self.i = i
self.l = []
@timeit
def profile_dict_of_dict():
return dict((i, {'i': i, 'l': []}) for i in xrange(ITER_COUNT))
@timeit
def profile_list_of_dict():
return [{'i': i, 'l': []} for i in xrange(ITER_COUNT)]
@timeit
def profile_dict_of_obj():
return dict((i, Obj(i)) for i in xrange(ITER_COUNT))
@timeit
def profile_list_of_obj():
return [Obj(i) for i in xrange(ITER_COUNT)]
@timeit
def profile_dict_of_slotobj():
return dict((i, SlotObj(i)) for i in xrange(ITER_COUNT))
@timeit
def profile_list_of_slotobj():
return [SlotObj(i) for i in xrange(ITER_COUNT)]
if __== '__main__':
profile_dict_of_dict()
profile_list_of_dict()
profile_dict_of_obj()
profile_list_of_obj()
profile_dict_of_slotobj()
profile_list_of_slotobj()
結果:
hbrown@hbrown-lpt:~$ python ~/Dropbox/src/StackOverflow/1336791.py
profile_dict_of_dict ((), {}) 0:00:08.228094
profile_list_of_dict ((), {}) 0:00:06.040870
profile_dict_of_obj ((), {}) 0:00:11.481681
profile_list_of_obj ((), {}) 0:00:10.893125
profile_dict_of_slotobj ((), {}) 0:00:06.381897
profile_list_of_slotobj ((), {}) 0:00:05.860749
質問はありません。
データがあり、他の属性はありません(メソッドも何もありません)。したがって、データコンテナー(この場合は辞書)があります。
私は通常、データモデリングの観点から考えることを好みます。何らかの大きなパフォーマンスの問題がある場合は、抽象化で何かを放棄することができますが、それは非常に正当な理由がある場合のみです。
プログラミングはすべて複雑さの管理に関するものであり、正しい抽象化を維持することは、そのような結果を達成するための最も有用な方法の1つであることが非常に多くあります。
reasonsの理由については、オブジェクトが遅いので、測定が正しくないと思います。
forループ内で実行する割り当てが少なすぎるため、表示されるのは、dict(組み込みオブジェクト)と「カスタム」オブジェクトをインスタンス化するのに必要な時間です。言語の観点からは同じですが、実装はまったく異なります。
その後、エンドメンバーはディクショナリ内で維持されるため、割り当て時間は両方でほぼ同じになるはずです。
データ構造に参照サイクルが含まれていない場合に、メモリ使用量を削減する別の方法があります。
2つのクラスを比較してみましょう。
class DataItem:
__slots__ = ('name', 'age', 'address')
def __init__(self, name, age, address):
self.name = name
self.age = age
self.address = address
そして
$ pip install recordclass
>>> from recordclass import structclass
>>> DataItem2 = structclass('DataItem', 'name age address')
>>> inst = DataItem('Mike', 10, 'Cherry Street 15')
>>> inst2 = DataItem2('Mike', 10, 'Cherry Street 15')
>>> print(inst2)
>>> print(sys.getsizeof(inst), sys.getsizeof(inst2))
DataItem(name='Mike', age=10, address='Cherry Street 15')
64 40
structclass
ベースのクラスはサイクリックガベージコレクションをサポートしていないため可能になりましたが、これはそのような場合には必要ありません。
__slots__
ベースのクラスに対する利点も1つあります。追加の属性を追加できます。
>>> DataItem3 = structclass('DataItem', 'name age address', usedict=True)
>>> inst3 = DataItem3('Mike', 10, 'Cherry Street 15')
>>> inst3.hobby = ['drawing', 'singing']
>>> print(inst3)
>>> print(sizeof(inst3), 'has dict:', bool(inst3.__dict__))
DataItem(name='Mike', age=10, address='Cherry Street 15', **{'hobby': ['drawing', 'singing']})
48 has dict: True