शब्दकोश बनाम वस्तु - जो अधिक कुशल है और क्यों?


126

स्मृति उपयोग और सीपीयू खपत - शब्दकोश या वस्तु के संदर्भ में पायथन में अधिक कुशल क्या है?

पृष्ठभूमि: मुझे पायथन में बड़ी मात्रा में डेटा लोड करना होगा। मैंने एक ऑब्जेक्ट बनाया जो सिर्फ एक फील्ड कंटेनर है। 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

प्रश्न: क्या मैं कुछ गलत कर रहा हूं या शब्दकोश वस्तु से ज्यादा तेज है? यदि वास्तव में शब्दकोश बेहतर प्रदर्शन करता है, तो क्या कोई समझा सकता है कि क्यों?


10
जब आप उस तरह से बड़े क्रम बनाते हैं तो आपको रेंज के बजाय xrange का उपयोग करना चाहिए। बेशक, जब से आप निष्पादन समय के सेकंड के साथ काम कर रहे हैं, तो इससे बहुत फर्क नहीं पड़ेगा, लेकिन फिर भी, यह एक अच्छी आदत है।
Xiong Chiamiov

2
जब तक कि यह python3 नहीं है
बार्नी

जवाबों:


157

क्या आपने प्रयोग करने की कोशिश की है __slots__?

से प्रलेखन :

डिफ़ॉल्ट रूप से, पुरानी और नई-शैली दोनों वर्गों के उदाहरणों में विशेषता भंडारण के लिए एक शब्दकोश है। यह बहुत कम उदाहरण चर वाली वस्तुओं के लिए स्थान बर्बाद करता है। बड़ी संख्या में उदाहरण बनाते समय अंतरिक्ष की खपत तीव्र हो सकती है।

__slots__नई शैली की श्रेणी परिभाषा में परिभाषित करके डिफ़ॉल्ट को ओवरराइड किया जा सकता है । __slots__घोषणा हर चर के लिए एक मूल्य धारण करने के लिए प्रत्येक उदाहरण में उदाहरण चर और भंडार सिर्फ पर्याप्त जगह का एक अनुक्रम लेता है। अंतरिक्ष सहेजा जाता है क्योंकि __dict__प्रत्येक उदाहरण के लिए नहीं बनाया गया है।

तो क्या इससे समय के साथ-साथ मेमोरी भी बचती है?

मेरे कंप्यूटर पर तीन तरीकों की तुलना:

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_onymtuple.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__)

टप्ले परीक्षण सहित सीपीथॉन 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__


2
यह बहुत अच्छा है - धन्यवाद! मैंने अपनी मशीन पर समान कोशिश की है - स्लॉट्स के साथ ऑब्जेक्ट सबसे कुशल दृष्टिकोण है (मुझे ~ 7sec मिला)।
tkokoszka

6
स्लॉट के साथ ऑब्जेक्ट्स के लिए एक क्लास फैक्टरी tuples, docs.python.org/library/collections.html#collections.ametuple नाम भी हैं । यह निश्चित रूप से neater और शायद और भी अधिक अनुकूलित है।
जोचेन रिट्जेल

मैंने टुपल्स नाम का परीक्षण किया, और परिणामों के साथ उत्तर को अपडेट किया।
कोडेपी

1
मैंने आपका कोड कुछ बार चलाया और मेरे परिणामों को अलग-अलग आश्चर्यचकित किया - स्लॉट्स = 3sec obj = 11sec dict = 12sec nametuple = 16sec। मैं Win7 64bit पर CPython 2.6.6 का उपयोग कर रहा हूँ
जोनाथन

पंचलाइन पर जोर देने के लिए - नाम्टुपल को सर्वश्रेष्ठ के बजाय सबसे खराब परिणाम मिला
जोनाथन

15

किसी ऑब्जेक्ट में विशेषता का उपयोग करना पर्दे के पीछे डिक्शनरी एक्सेस का उपयोग करता है - इसलिए विशेषता एक्सेस का उपयोग करके आप अतिरिक्त ओवरहेड जोड़ रहे हैं। इसके अलावा वस्तु मामले में, आप अतिरिक्त ओवरहेड की वजह से अतिरिक्त स्मृति आवंटन और कोड निष्पादन ( __init__विधि का उदाहरण ) के कारण कर रहे हैं।

अपने कोड में, अगर oएक है Objउदाहरण के लिए, o.attrके बराबर है o.__dict__['attr']अतिरिक्त भूमि के ऊपर की एक छोटी राशि के साथ।


क्या आपने यह परीक्षण किया? o.__dict__["attr"]अतिरिक्त उपरि के साथ एक है, एक अतिरिक्त बाइटकोड सेशन ले रहा है; obj.attr तेज है। (निश्चित रूप से विशेषता अभिगम सदस्यता सदस्यता की तुलना में धीमा नहीं होने जा रहा है - यह एक महत्वपूर्ण, भारी अनुकूलित कोड पथ है।)
ग्लेन मेनार्ड

2
जाहिर है अगर आप वास्तव में है ओ .__ dict __ [ "attr"] यह धीमी हो जाएगा - मैं केवल यह कहना है कि ऐसा लगता है कि के बराबर था, न कि यह कि रास्ते में वास्तव में लागू किया गया था मतलब है। मुझे लगता है कि यह मेरे शब्दांकन से स्पष्ट नहीं है। मैंने अन्य कारकों का भी उल्लेख किया जैसे स्मृति आवंटन, निर्माता कॉल समय आदि
विनय साजिप

क्या यह अभी भी 11 साल बाद python3 के हाल के संस्करणों के साथ मामला है?
matanster

9

क्या आपने नामांकित का उपयोग करने पर विचार किया है ? ( अजगर 2.4 / 2.5 के लिए लिंक )

यह संरचित डेटा का प्रतिनिधित्व करने का नया मानक तरीका है जो आपको एक टपल का प्रदर्शन और एक वर्ग की सुविधा प्रदान करता है।

यह केवल शब्दकोशों के साथ तुलना में नकारात्मक है (जैसे कि टुपल्स) यह आपको सृजन के बाद विशेषताओं को बदलने की क्षमता नहीं देता है।


5

यहां अजगर 3.6.1 के लिए @hughdbrown जवाब की एक प्रति है, मैंने गिनती 5x बड़ी कर दी है और प्रत्येक रन के अंत में अजगर प्रक्रिया की मेमोरी फ़ुटप्रिंट का परीक्षण करने के लिए कुछ कोड जोड़ा है।

इससे पहले कि डाउनवोटर्स इसमें हों, सलाह दें कि वस्तुओं के आकार को गिनने की यह विधि सटीक नहीं है।

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. dicts सबसे तेज़ हैं, लेकिन सबसे अधिक मेमोरी का उपयोग करते हैं।

यार, तुम्हें इसे एक प्रश्न में बदल देना चाहिए। मैंने इसे अपने कंप्यूटर पर भी चलाया, बस यह सुनिश्चित करने के लिए (मेरे पास psutil स्थापित नहीं था, इसलिए मैंने वह हिस्सा निकाल लिया)। वैसे भी, यह मेरे लिए चौंकाने वाला है, और इसका मतलब है कि मूल प्रश्न का पूरी तरह से उत्तर नहीं दिया गया है। अन्य सभी उत्तर "नामपट्ट महान है" और " स्लॉट्स का उपयोग करें " जैसे हैं, और जाहिर है हर बार उनके लिए एक ब्रांड नई तानाशाह वस्तु तेजी से होती है? मुझे लगता है कि dicts वास्तव में अच्छी तरह से अनुकूलित हैं?
मल्टीहंटर

1
ऐसा लगता है कि मेक फंक्शन का परिणाम एक स्ट्रिंग लौट रहा है। यदि आप एक खाली सूची लौटाते हैं, तो इसके बजाय, परिणाम मोटे तौर पर ह्यूथब्रोन्स से अजगर 2 से मेल खाते हैं। नामपट्ट को छोड़कर हमेशा स्लोटऑब्ज की तुलना में धीमा होता है :(
मल्टीहंटर

एक छोटी सी समस्या हो सकती है: मेक इन प्रत्येक दौर में अलग-अलग गति के साथ चल सकता है क्योंकि पायथन में तार लगाए जाते हैं - लेकिन शायद मैं गलत हूं।
बार्नी

@BarnabasSzabolcs एक नया स्ट्रिंग हर बार क्योंकि यह मूल्य में स्थानापन्न करने है बनाना चाहिए% i "यह एक नमूना स्ट्रिंग% s है"
जारोड Chesney

हां, यह लूप के भीतर सही है, लेकिन दूसरे टेस्ट में मैं फिर से 0 से शुरू होता है।
बार्नी

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

परिणाम:

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

कोई सवाल नहीं है।
आपके पास कोई अन्य गुण (कोई विधियाँ, कुछ नहीं) के साथ डेटा है। इसलिए आपके पास एक डेटा कंटेनर है (इस मामले में, एक शब्दकोश)।

मैं आमतौर पर डेटा मॉडलिंग के मामले में सोचना पसंद करता हूं । अगर कुछ विशाल प्रदर्शन मुद्दा है, तो मैं अमूर्त में कुछ छोड़ सकता हूं, लेकिन केवल बहुत अच्छे कारणों के साथ।
प्रोग्रामिंग सभी प्रबंधन जटिलता के बारे में है, और सही अमूर्तता को बनाए रखना है बहुत अक्सर इस तरह के परिणाम प्राप्त करने का सबसे उपयोगी तरीका है।

एक वस्तु धीमी होने के कारणों के बारे में , मुझे लगता है कि आपका माप सही नहीं है।
आप लूप के लिए बहुत कम कार्य कर रहे हैं, और इसलिए आप जो देखते हैं वह एक तानाशाही (आंतरिक वस्तु) और एक "कस्टम" ऑब्जेक्ट को तुरंत अलग करने के लिए आवश्यक अलग समय है। यद्यपि भाषा के दृष्टिकोण से वे समान हैं, लेकिन उनका काफी अलग कार्यान्वयन है।
उसके बाद, असाइनमेंट का समय दोनों के लिए लगभग समान होना चाहिए, क्योंकि अंत में सदस्यों को एक शब्दकोश के अंदर बनाए रखा जाता है।


0

यदि मेमोरी संरचना में संदर्भ चक्र शामिल नहीं हैं, तो मेमोरी उपयोग को कम करने का एक और तरीका है।

आइए दो वर्गों की तुलना करें:

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__वर्ग भी हैं: आप अतिरिक्त विशेषताओं को जोड़ने में सक्षम हैं:

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

यहाँ @ जर्दोद-चेसनी की बहुत अच्छी पटकथा के मेरे टेस्ट रन हैं। तुलना के लिए, मैं इसे "रेंज" के साथ "रेंज" के साथ python2 के खिलाफ भी चलाता हूं।

जिज्ञासा से, मैंने तुलना के लिए ऑर्डरडीडिक्ट (ऑर्डिक्ट) के साथ भी इसी तरह के परीक्षण जोड़े।

अजगर 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

इसलिए, दोनों प्रमुख संस्करणों पर, @ जारोड-चेसनी के निष्कर्ष अभी भी अच्छे दिख रहे हैं।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.