Dictionary vs Object - ข้อใดมีประสิทธิภาพมากกว่าและเพราะเหตุใด


126

อะไรที่มีประสิทธิภาพมากกว่าใน Python ในแง่ของการใช้หน่วยความจำและการใช้ CPU - พจนานุกรมหรือวัตถุ?

ความเป็นมา: ฉันต้องโหลดข้อมูลจำนวนมากลงใน Python ฉันสร้างวัตถุที่เป็นเพียงคอนเทนเนอร์ฟิลด์ การสร้างอินสแตนซ์ 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 แทน range เมื่อสร้างลำดับขนาดใหญ่เช่นนั้น แน่นอนว่าเนื่องจากคุณต้องรับมือกับเวลาดำเนินการเพียงไม่กี่วินาทีจึงไม่สามารถสร้างความแตกต่างได้มากนัก แต่ก็ยังเป็นนิสัยที่ดี
Xiong Chiamiov

2
เว้นแต่จะเป็น python3
Barney

คำตอบ:


158

คุณลองใช้แล้ว__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_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__.


2
เยี่ยมมาก - ขอบคุณ! ฉันได้ลองสิ่งเดียวกันบนเครื่องของฉัน - วัตถุที่มีช่องเป็นวิธีที่มีประสิทธิภาพมากที่สุด (ฉันได้รับ ~ 7 วินาที)
tkokoszka

6
นอกจากนี้ยังมีชื่อ tuples docs.python.org/library/collections.html#collections.namedtuple คลาสแฟคทอรีสำหรับอ็อบเจ็กต์ที่มีสล็อต แน่นอนกว่าและอาจปรับให้เหมาะสมยิ่งขึ้น
Jochen Ritzel

ฉันทดสอบสิ่งที่ตั้งชื่อแล้วเช่นกันและอัปเดตคำตอบพร้อมผลลัพธ์
codeape

1
ฉันรันโค้ดของคุณสองสามครั้งและรู้สึกประหลาดใจที่ผลลัพธ์ของฉันแตกต่างกัน - สล็อต = 3sec obj = 11sec dict = 12sec namedtuple = 16sec ฉันใช้ CPython 2.6.6 บน Win7 64bit
Jonathan

เพื่อเน้นการชกไลน์ - ชื่อทูเปิลได้ผลลัพธ์ที่แย่ที่สุดแทนที่จะเป็นดีที่สุด
โจนาธาน

16

การเข้าถึงแอตทริบิวต์ในออบเจ็กต์จะใช้การเข้าถึงพจนานุกรมเบื้องหลังดังนั้นการใช้การเข้าถึงแอตทริบิวต์จะเป็นการเพิ่มค่าโสหุ้ยพิเศษ นอกจากนี้ในกรณีวัตถุคุณมีค่าใช้จ่ายเพิ่มเติมเนื่องจากเช่นการจัดสรรหน่วยความจำเพิ่มเติมและการเรียกใช้รหัส (เช่น__init__วิธีการ)

ในโค้ดของคุณถ้าoเป็นObjอินสแตนซ์o.attrจะเทียบเท่าo.__dict__['attr']กับค่าโสหุ้ยพิเศษจำนวนเล็กน้อย


คุณทดสอบสิ่งนี้หรือไม่? o.__dict__["attr"]คือคนที่มีค่าโสหุ้ยพิเศษโดยใช้ตัวเลือก bytecode พิเศษ obj.attr เร็วกว่า (แน่นอนว่าการเข้าถึงแอตทริบิวต์จะไม่ช้าไปกว่าการเข้าถึงการสมัครสมาชิก แต่เป็นเส้นทางรหัสที่สำคัญและได้รับการปรับให้เหมาะสมอย่างมาก)
Glenn Maynard

2
เห็นได้ชัดว่าถ้าคุณทำ o .__ dict __ ["attr"] มันจะช้าลง - ฉันแค่ตั้งใจจะบอกว่ามันเทียบเท่ากับสิ่งนั้นไม่ใช่ว่ามันถูกนำไปใช้ในลักษณะนั้นอย่างแน่นอน ฉันเดาว่ามันไม่ชัดเจนจากคำพูดของฉัน ฉันยังกล่าวถึงปัจจัยอื่น ๆ เช่นการจัดสรรหน่วยความจำเวลาเรียกตัวสร้างเป็นต้น
Vinay Sajip

ยังคงเป็นเช่นนี้กับ python3 เวอร์ชันล่าสุด 11 ปีต่อมาหรือไม่?
matanster

9

คุณเคยคิดจะใช้namestupleหรือไม่? ( ลิงค์สำหรับ python 2.4 / 2.5 )

เป็นวิธีมาตรฐานใหม่ในการนำเสนอข้อมูลที่มีโครงสร้างซึ่งให้ประสิทธิภาพของทูเพิลและความสะดวกสบายของคลาส

ข้อเสียเพียงอย่างเดียวเมื่อเทียบกับพจนานุกรมคือ (เช่น tuples) ไม่ได้ให้ความสามารถในการเปลี่ยนแอตทริบิวต์หลังการสร้าง


6

นี่คือสำเนาของคำตอบ @hughdbrown สำหรับ python 3.6.1 ฉันได้ทำการนับให้ใหญ่ขึ้น 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. คำสั่งนั้นเร็วที่สุด แต่ใช้หน่วยความจำมากที่สุด

ผู้ชายคุณควรเปลี่ยนเป็นคำถาม ฉันใช้งานบนคอมพิวเตอร์ของฉันเองด้วยเพื่อให้แน่ใจ (ฉันไม่ได้ติดตั้ง psutil ดังนั้นฉันจึงถอดส่วนนั้นออก) อย่างไรก็ตามนี่เป็นเรื่องที่ทำให้ฉันงงและหมายความว่าคำถามเดิมยังไม่ได้รับคำตอบทั้งหมด คำตอบอื่น ๆ ทั้งหมดก็เช่น "namedtuple is great" และ "use slots " และดูเหมือนว่าวัตถุ dict ใหม่เอี่ยมทุกครั้งจะเร็วกว่าพวกเขา? ฉันเดาว่าคำสั่งนั้นได้รับการปรับให้เหมาะสมจริงๆ?
Multihunter

1
ดูเหมือนว่าจะเป็นผลมาจากฟังก์ชัน makeL ที่ส่งคืนสตริง หากคุณส่งคืนรายการว่างแทนผลลัพธ์จะตรงกับ hughdbrown ของจาก python2 แทน ยกเว้น namestuples จะช้ากว่า SlotObj เสมอ :(
Multihunter

อาจมีปัญหาเล็กน้อย: makeL สามารถทำงานด้วยความเร็วที่แตกต่างกันในแต่ละรอบ '@timeit' เนื่องจากสตริงถูกแคชใน python - แต่ฉันอาจจะคิดผิด
Barney

@BarnabasSzabolcs ควรสร้างสตริงใหม่ทุกครั้งเพราะต้องแทนที่ด้วยค่า "นี่คือสตริงตัวอย่าง% s"% i
Jarrod Chesney

ใช่นั่นเป็นความจริงภายในลูป แต่ในการทดสอบครั้งที่สองฉันเริ่มจาก 0 อีกครั้ง
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()

ผล:

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

ไม่มีคำถามอะไร
คุณมีข้อมูลโดยไม่มีแอตทริบิวต์อื่น ๆ (ไม่มีวิธีการไม่มีอะไรเลย) ดังนั้นคุณจึงมีที่เก็บข้อมูล (ในกรณีนี้คือพจนานุกรม)

ฉันมักจะชอบที่จะคิดในแง่ของการสร้างแบบจำลองข้อมูล หากมีปัญหาด้านประสิทธิภาพอย่างมากฉันสามารถละทิ้งบางสิ่งในนามธรรมได้ แต่ด้วยเหตุผลที่ดีเท่านั้น
การเขียนโปรแกรมเป็นเรื่องของการจัดการความซับซ้อนและการรักษานามธรรมที่ถูกต้องมักเป็นวิธีที่มีประโยชน์มากที่สุดวิธีหนึ่งในการบรรลุผลลัพธ์ดังกล่าว

เกี่ยวกับสาเหตุที่วัตถุทำงานช้าลงฉันคิดว่าการวัดของคุณไม่ถูกต้อง
คุณทำงานที่ได้รับมอบหมายน้อยเกินไปใน for loop ดังนั้นสิ่งที่คุณเห็นจึงมีเวลาที่แตกต่างกันในการสร้างอินสแตนซ์ของ dict (วัตถุภายใน) และอ็อบเจ็กต์ "กำหนดเอง" แม้ว่าจากมุมมองของภาษาจะเหมือนกัน แต่ก็มีการใช้งานที่แตกต่างกันมาก
หลังจากนั้นเวลาในการมอบหมายงานควรจะใกล้เคียงกันสำหรับทั้งคู่เนื่องจากในส่วนสุดท้ายสมาชิกจะอยู่ในพจนานุกรม


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

นี่คือการทดสอบสคริปต์ที่ดีมากของ @ Jarrod-Chesney สำหรับการเปรียบเทียบฉันยังรันกับ python2 ด้วย "range" แทนที่ด้วย "xrange"

ด้วยความอยากรู้ฉันได้เพิ่มการทดสอบที่คล้ายกันกับ OrderDict (คำสั่ง) เพื่อเปรียบเทียบ

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

ดังนั้นในทั้งสองเวอร์ชันหลักข้อสรุปของ @ Jarrod-Chesney ยังคงดูดี

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.