Sözlük vs Nesne - hangisi daha verimli ve neden?


126

Python'da bellek kullanımı ve CPU tüketimi açısından daha verimli olan nedir - Sözlük veya Nesne?

Arka plan: Python'a büyük miktarda veri yüklemem gerekiyor. Sadece alan kabı olan bir nesne yarattım. 4M örnekleri oluşturmak ve bunları bir sözlüğe koymak yaklaşık 10 dakika ve ~ 6 GB bellek sürdü. Sözlük hazır olduktan sonra ona erişim göz açıp kapayıncaya kadar olur.

Örnek: Performansı kontrol etmek için aynı şeyi yapan iki basit program yazdım - biri nesneler, diğeri sözlük kullanıyor:

Nesne (yürütme süresi ~ 18sn):

class Obj(object):
  def __init__(self, i):
    self.i = i
    self.l = []
all = {}
for i in range(1000000):
  all[i] = Obj(i)

Sözlük (yürütme süresi ~ 12sn):

all = {}
for i in range(1000000):
  o = {}
  o['i'] = i
  o['l'] = []
  all[i] = o

Soru: Yanlış bir şey mi yapıyorum yoksa sözlük nesneden daha mı hızlı? Eğer sözlük gerçekten daha iyi performans gösteriyorsa, birisi nedenini açıklayabilir mi?


10
Bunun gibi büyük diziler oluştururken gerçekten aralık yerine xrange kullanmalısınız. Elbette, saniyeler süren infaz süresiyle uğraştığınız için, pek bir fark yaratmayacak, ama yine de bu iyi bir alışkanlık.
Xiong Chiamiov

2
python3 değilse
Barney

Yanıtlar:


158

Kullanmayı denedin mi __slots__ mi

Gönderen belgeler :

Varsayılan olarak, hem eski hem de yeni stil sınıflarının örnekleri, öznitelik depolaması için bir sözlüğe sahiptir. Bu, çok az örnek değişkenine sahip nesneler için yer israfına neden olur. Çok sayıda örnek oluştururken alan tüketimi akut hale gelebilir.

Varsayılan, __slots__yeni stil sınıf tanımında tanımlanarak geçersiz kılınabilir . __slots__Bildirimi, her bir değişken için bir değeri saklamak için, her bir durumda, örneğin değişken ve rezerv yeterli alan bir dizi alır. __dict__Her örnek için oluşturulmadığından alan kaydedilir .

Peki bu hem zamandan hem de bellekten tasarruf sağlıyor mu?

Bilgisayarımdaki üç yaklaşımı karşılaştırıyorum:

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'da desteklenir):

import collections

Obj = collections.namedtuple('Obj', 'i l')

all = {}
for i in range(1000000):
  all[i] = Obj(i, [])

Karşılaştırmayı çalıştırın (CPython 2.5 kullanarak):

$ 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__)

Adlandırılmış tuple testi dahil olmak üzere CPython 2.6.2'yi kullanma:

$ 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)

Yani evet (gerçekten sürpriz değil), kullanmak __slots__bir performans optimizasyonudur. Adlandırılmış bir demet kullanmak __slots__,.


2
Bu harika - teşekkürler! Aynısını makinemde de denedim - yuvalı nesne en verimli yaklaşımdır (~ 7sn aldım).
tkokoszka

6
Ayrıca yuvalı nesneler için bir sınıf fabrikası olan docs.python.org/library/collections.html#collections.namedtuple adlı adlandırılmış tuples da vardır . Kesinlikle daha temiz ve belki daha da optimize edilmiş.
Jochen Ritzel

Adlandırılmış demetleri de test ettim ve cevabı sonuçlarla güncelledim.
codeape

1
Kodunuzu birkaç kez çalıştırdım ve sonuçlarımın farklı olmasına şaşırdım - slots = 3sec obj = 11sec dict = 12sn namedtuple = 16sec. Win7 64bit üzerinde CPython 2.6.6 kullanıyorum
Jonathan

Can alıcı noktayı vurgulamak için - nametuple en iyisi yerine en kötü sonuçları aldı
Jonathan

16

Bir nesnede öznitelik erişimi, perde arkasında sözlük erişimini kullanır - bu nedenle öznitelik erişimini kullanarak fazladan ek yük getirirsiniz. Ayrıca nesne durumunda, örneğin ek bellek tahsisleri ve kod çalıştırma (örneğin __init__yöntemin) nedeniyle ek yüke maruz kalıyorsunuz .

Kodunuzda, eğer obir Objörnek ise o.attr, o.__dict__['attr']az miktarda fazladan ek yüke eşdeğerdir .


Bunu test ettin mi? o.__dict__["attr"]fazladan ek yüke sahip olandır, fazladan bir bayt kodu op alır; obj.attr daha hızlıdır. (Elbette öznitelik erişimi abonelik erişiminden daha yavaş olmayacak - kritik, yoğun şekilde optimize edilmiş bir kod yolu.)
Glenn Maynard

2
Açıkçası, o .__ dict __ ["attr"] 'i gerçekten yaparsanız , daha yavaş olacaktır - sadece buna eşdeğer olduğunu söylemek istedim, tam olarak bu şekilde uygulandığını değil. Sanırım ifadelerime göre net değil. Bellek ayırmaları, kurucu çağrı süresi gibi diğer faktörlerden de bahsetmiştim.
Vinay Sajip

Bu, 11 yıl sonraki python3 sürümlerinde hala geçerli mi?
matanster


6

İşte python 3.6.1 için @hughdbrown cevabının bir kopyası, sayımı 5 kat artırdım ve her çalışmanın sonunda python işleminin bellek ayak izini test etmek için bazı kodlar ekledim.

Olumsuz oy verenler bunu yapmadan önce, nesnelerin boyutunu hesaplamanın bu yönteminin doğru olmadığını unutmayın.

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()

Ve bunlar benim sonuçlarım

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

Benim sonucum:

  1. Yuvalar en iyi bellek ayak izine sahiptir ve hız açısından makuldür.
  2. dicts en hızlısıdır, ancak en çok belleği kullanır.

Dostum, bunu bir soruya çevirmelisin. Ben de kendi bilgisayarımda çalıştırdım, sadece emin olmak için (psutil'i yüklemedim, bu yüzden o kısmı çıkardım). Her neyse, bu benim için şaşırtıcı ve orijinal sorunun tam olarak cevaplanmadığı anlamına geliyor. Diğer tüm yanıtlar "adlandırılmıştuple harika" ve " slotları kullan " gibi ve görünüşe göre yepyeni bir dikt nesnesi her seferinde onlardan daha hızlı mı? Sanırım diktatlar gerçekten iyi optimize edilmiş mi?
Multihunter

1
MakeL işlevinin bir dizge döndürmesinin sonucu gibi görünüyor. Bunun yerine boş bir liste döndürürseniz, sonuçlar python2'deki hughdbrown ile kabaca eşleşir. Adlandırılmış çiftler hariç her zaman SlotObj'den daha yavaştır :(
Multihunter

Küçük bir sorun olabilir: makeL her '@timeit' turunda farklı hızlarda çalışabilir çünkü dizeler python'da önbelleğe alınır - ama belki yanılıyorum.
Barney

@BarnabasSzabolcs, "Bu bir örnek dizedir% s" değerini değiştirmesi gerektiğinden her seferinde yeni bir dize oluşturmalıdır% i
Jarrod Chesney

Evet, döngü içinde bu doğru, ancak ikinci testte tekrar 0'dan başlıyorum.
Barney

4
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 __name__ == '__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()

Sonuçlar:

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

3

Soru yok.
Verileriniz var, başka nitelikler yok (yöntem yok, hiçbir şey yok). Dolayısıyla bir veri kabınız var (bu durumda, bir sözlük).

Genelde veri modelleme açısından düşünmeyi tercih ederim . Çok büyük bir performans sorunu varsa, soyutlamada bir şeyden vazgeçebilirim, ancak ancak çok iyi nedenlerle.
Programlama tamamen karmaşıklığı yönetmek ve doğru soyutlamayı sürdürmekle ilgilidir. çoğu zaman böyle bir sonucu elde etmenin en yararlı yollarından biridir.

Bir nesnenin daha yavaş olmasının nedenleri hakkında, ölçümünüzün doğru olmadığını düşünüyorum.
For döngüsü içinde çok az atama yapıyorsunuz ve bu nedenle orada gördüğünüz şey, bir dikteyi (içsel nesne) ve "özel" bir nesneyi somutlaştırmak için gereken farklı zamandır. Dil açısından aynı olsalar da, oldukça farklı bir uygulamaya sahiptirler.
Bundan sonra, son üyelerde olduğu gibi, atama süresi her ikisi için de hemen hemen aynı olmalıdır.


0

Veri yapısının referans döngüleri içermesi gerekiyorsa, bellek kullanımını azaltmanın başka bir yolu daha vardır.

İki sınıfı karşılaştıralım:

class DataItem:
    __slots__ = ('name', 'age', 'address')
    def __init__(self, name, age, address):
        self.name = name
        self.age = age
        self.address = address

ve

$ 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

Çünkü mümkün hale geldi structclasstabanlı sınıflar gibi durumlarda gerekli değildir halkalı çöp toplama, desteklemez.

Ayrıca, aşırı __slots__tabanlı sınıfın bir avantajı vardır : ekstra özellikler ekleyebilirsiniz:

>>> 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

0

İşte @ Jarrod-Chesney'in çok güzel senaryosunu test ettim. Karşılaştırma için, onu python2'ye karşı da "aralık" ile "xrange" ile değiştiriyorum.

Merakla, karşılaştırma için OrderedDict (ordict) ile benzer testler ekledim.

Python 3.6.9:

Time Taken = 0:00:04.971369,    profile_dict_of_nt,     Size = 944.27
Time Taken = 0:00:05.743104,    profile_list_of_nt,     Size = 1,066.93
Time Taken = 0:00:02.524507,    profile_dict_of_dict,   Size = 1,920.35
Time Taken = 0:00:02.123801,    profile_list_of_dict,   Size = 1,760.9
Time Taken = 0:00:05.374294,    profile_dict_of_obj,    Size = 1,532.12
Time Taken = 0:00:04.517245,    profile_list_of_obj,    Size = 1,441.04
Time Taken = 0:00:04.590298,    profile_dict_of_slot,   Size = 1,030.09
Time Taken = 0:00:04.197425,    profile_list_of_slot,   Size = 870.67

Time Taken = 0:00:08.833653,    profile_ordict_of_ordict, Size = 3,045.52
Time Taken = 0:00:11.539006,    profile_list_of_ordict, Size = 2,722.34
Time Taken = 0:00:06.428105,    profile_ordict_of_obj,  Size = 1,799.29
Time Taken = 0:00:05.559248,    profile_ordict_of_slot, Size = 1,257.75

Python 2.7.15+:

Time Taken = 0:00:05.193900,    profile_dict_of_nt,     Size = 906.0
Time Taken = 0:00:05.860978,    profile_list_of_nt,     Size = 1,177.0
Time Taken = 0:00:02.370905,    profile_dict_of_dict,   Size = 2,228.0
Time Taken = 0:00:02.100117,    profile_list_of_dict,   Size = 2,036.0
Time Taken = 0:00:08.353666,    profile_dict_of_obj,    Size = 2,493.0
Time Taken = 0:00:07.441747,    profile_list_of_obj,    Size = 2,337.0
Time Taken = 0:00:06.118018,    profile_dict_of_slot,   Size = 1,117.0
Time Taken = 0:00:04.654888,    profile_list_of_slot,   Size = 964.0

Time Taken = 0:00:59.576874,    profile_ordict_of_ordict, Size = 7,427.0
Time Taken = 0:10:25.679784,    profile_list_of_ordict, Size = 11,305.0
Time Taken = 0:05:47.289230,    profile_ordict_of_obj,  Size = 11,477.0
Time Taken = 0:00:51.485756,    profile_ordict_of_slot, Size = 11,193.0

Dolayısıyla, her iki büyük sürümde de @ Jarrod-Chesney'nin sonuçları hala iyi görünüyor.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.