programing

사전 대 객체 - 어떤 것이 더 효율적이고 왜?

minimums 2023. 7. 17. 20:51
반응형

사전 대 객체 - 어떤 것이 더 효율적이고 왜?

Python에서 메모리 사용 및 CPU 소비 측면에서 사전과 객체 중 어떤 것이 더 효율적입니까?

배경:저는 엄청난 양의 데이터를 파이썬에 로드해야 합니다.필드 컨테이너에 불과한 개체를 만들었습니다.4M개의 인스턴스를 생성하여 사전에 저장하는 데 약 10분이 소요되었으며 최대 6GB의 메모리가 필요했습니다.사전이 준비된 후에는 사전에 액세스하는 것이 눈 깜짝할 사이에 가능합니다.

예:성능을 확인하기 위해 동일한 작업을 수행하는 두 가지 간단한 프로그램을 작성했습니다. 하나는 객체를 사용하는 것이고 다른 하나는 사전을 사용하는 것입니다.

개체(실행 시간 ~ 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__각 인스턴스에 대해 생성되지 않습니다.

그러면 이것이 기억뿐만 아니라 시간도 절약할 수 있을까요?

내 컴퓨터의 세 가지 접근 방식 비교:

test_interval의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_timeout.py:

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

test_nametuple.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']약간의 여분의 간접비로.

이름이 붙은 튜플을 사용하는 것을 고려해 본 적이 있습니까?(python 2.4/2.5용 링크)

이것은 튜플의 성능과 클래스의 편리성을 제공하는 구조화된 데이터를 표현하는 새로운 표준 방법입니다.

사전과 비교했을 때 단점은 (튜플과 마찬가지로) 생성 후 속성을 변경할 수 없다는 것입니다.

여기 python 3.6.1에 대한 @hughbrown 답변 복사본이 있습니다. 카운트를 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

제 결론은 다음과 같습니다.

  1. 슬롯은 메모리 설치 공간이 가장 좋고 속도가 합리적입니다.
  2. 딕트가 가장 빠르지만 메모리를 가장 많이 사용합니다.
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()

결과:

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

의문의 여지가 없습니다.
다른 속성(방법 없음, 없음)이 없는 데이터가 있습니다.따라서 데이터 컨테이너(이 경우 사전)가 있습니다.

저는 보통 데이터 모델링 측면에서 생각하는 것을 선호합니다.만약 엄청난 성능 문제가 있다면, 추상화에서 무언가를 포기할 수 있지만, 아주 좋은 이유가 있을 뿐입니다.
프로그래밍은 복잡성을 관리하는 것이 전부이며, 정확한 추상화를 유지하는 것은 종종 그러한 결과를 달성하는 가장 유용한 방법 중 하나입니다.

물체가 느린 이유에 대해서는, 당신의 측정이 정확하지 않은 것 같습니다.
for 루프 내에서 할당을 너무 적게 수행하므로 딕트(기본 개체)와 "사용자 지정" 개체를 인스턴스화하는 데 필요한 시간이 다릅니다.언어의 관점에서는 동일하지만, 상당히 다른 구현을 가지고 있습니다.
그 후, 최종 구성원이 사전 내에서 유지되는 것처럼, 할당 시간은 둘 다 거의 동일해야 합니다.

이것은 제가 @Jarrod-Chesney의 아주 멋진 대본을 시험해 본 것입니다.비교를 위해 "range"가 "xrange"로 대체된 python2에 대해서도 실행합니다.

호기심으로 OrderDict(주문형)와 유사한 테스트를 비교하기 위해 추가했습니다.

파이썬 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

파이썬 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

그래서 두 주요 버전 모두에서 @Jarrod-Chesney의 결론은 여전히 좋아 보입니다.

데이터 구조에 참조 주기가 포함되어 있지 않은 경우 레코드 클래스 라이브러리의 도움으로 메모리 사용량을 줄일 수 있는 또 다른 방법이 있습니다.

두 클래스를 비교해 보겠습니다.

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 make_dataclass
>>> DataItem2 = make_dataclass('DataItem', 'name age address')
>>> inst = DataItem('Mike', 10, 'Cherry Street 15')
>>> inst2 = DataItem2('Mike', 10, 'Cherry Street 15')
>>> print(inst2)
DataItem(name='Mike', age=10, address='Cherry Street 15')
>>> print(sys.getsizeof(inst), sys.getsizeof(inst2))
64 40

이후로 가능해졌습니다.dataobject기반 하위 클래스는 순환 가비지 수집을 지원하지 않으므로 이러한 경우에는 필요하지 않습니다.

언급URL : https://stackoverflow.com/questions/1336791/dictionary-vs-object-which-is-more-efficient-and-why

반응형