Kamus vs Objek - mana yang lebih efisien dan mengapa?

126

Apa yang lebih efisien dengan Python dalam hal penggunaan memori dan konsumsi CPU - Kamus atau Objek?

Latar belakang: Saya harus memuat data dalam jumlah besar ke Python. Saya membuat sebuah objek yang hanya sebuah wadah bidang. Membuat 4 juta instance dan memasukkannya ke dalam kamus membutuhkan waktu sekitar 10 menit dan ~ 6GB memori. Setelah kamus siap, mengaksesnya dalam sekejap mata.

Contoh: Untuk memeriksa kinerja saya menulis dua program sederhana yang melakukan hal yang sama - satu menggunakan objek, kamus lain:

Objek (waktu eksekusi ~ 18 detik):

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

Kamus (waktu eksekusi ~ 12 detik):

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

Pertanyaan: Apakah saya melakukan sesuatu yang salah atau kamus lebih cepat dari objek? Jika kamus bekerja lebih baik, dapatkah seseorang menjelaskan mengapa?

tkokoszka
sumber
10
Anda harus benar-benar menggunakan xrange daripada range saat membuat urutan besar seperti itu. Tentu saja, karena Anda berurusan dengan detik waktu eksekusi, itu tidak akan membuat banyak perbedaan, tetapi tetap saja, itu adalah kebiasaan yang baik.
Xiong Chiamiov
2
kecuali jika itu adalah python3
Barney

Jawaban:

157

Sudahkah Anda mencoba menggunakan __slots__?

Dari dokumentasi :

Secara default, instance dari kelas gaya lama dan baru memiliki kamus untuk penyimpanan atribut. Ini membuang ruang untuk objek yang memiliki sangat sedikit variabel instan. Konsumsi ruang dapat menjadi akut saat membuat instance dalam jumlah besar.

Defaultnya bisa diganti dengan mendefinisikan __slots__dalam definisi kelas gaya baru. The __slots__deklarasi mengambil urutan variabel instance dan cadangan cukup ruang di setiap contoh untuk mengadakan nilai untuk setiap variabel. Ruang disimpan karena __dict__tidak dibuat untuk setiap contoh.

Jadi apakah ini menghemat waktu dan juga memori?

Membandingkan tiga pendekatan di komputer saya:

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 (didukung di 2.6):

import collections

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

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

Jalankan benchmark (menggunakan 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__)

Menggunakan CPython 2.6.2, termasuk tes tuple bernama:

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

Jadi ya (tidak terlalu mengejutkan), menggunakan __slots__adalah pengoptimalan kinerja. Menggunakan tupel bernama memiliki kinerja yang mirip dengan __slots__.

codeape
sumber
2
Bagus sekali - terima kasih! Saya telah mencoba hal yang sama pada mesin saya - objek dengan slot adalah pendekatan yang paling efisien (saya mendapat ~ 7 detik).
tkokoszka
6
Ada juga tupel bernama, docs.python.org/library/collections.html#collections.namedtuple , pabrik kelas untuk objek dengan slot. Ini pasti lebih rapi dan bahkan mungkin lebih dioptimalkan.
Jochen Ritzel
Saya menguji tupel bernama juga, dan memperbarui jawabannya dengan hasilnya.
codeape
1
Saya menjalankan kode Anda beberapa kali dan terkejut hasil saya berbeda - slots = 3sec obj = 11sec dict = 12sec bernamatuple = 16sec. Saya menggunakan CPython 2.6.6 pada Win7 64bit
Jonathan
Untuk menekankan bagian lucunya - Namedtuple mendapat hasil terburuk, bukan yang terbaik
Jonathan
15

Akses atribut dalam suatu objek menggunakan akses kamus di belakang layar - jadi dengan menggunakan akses atribut Anda menambahkan biaya tambahan. Ditambah dalam kasus objek, Anda menimbulkan overhead tambahan karena misalnya alokasi memori tambahan dan eksekusi kode (misalnya __init__metode).

Dalam kode Anda, jika oadalah sebuah Objinstance, o.attrsetara o.__dict__['attr']dengan sejumlah kecil biaya tambahan.

Vinay Sajip
sumber
Apakah Anda menguji ini? o.__dict__["attr"]adalah yang memiliki overhead ekstra, mengambil op bytecode ekstra; obj.attr lebih cepat. (Tentu saja akses atribut tidak akan lebih lambat daripada akses berlangganan - ini adalah jalur kode yang sangat penting dan dioptimalkan.)
Glenn Maynard
2
Jelas jika Anda benar - benar melakukan o .__ dict __ ["attr"] itu akan lebih lambat - Saya hanya bermaksud mengatakan bahwa itu setara dengan itu, bukan bahwa itu diterapkan persis seperti itu. Saya kira itu tidak jelas dari kata-kata saya. Saya juga menyebutkan faktor-faktor lain seperti alokasi memori, waktu panggilan konstruktor, dll.
Vinay Sajip
Apakah ini masih terjadi pada python3 versi terbaru, 11 tahun kemudian?
matanster
9

Sudahkah Anda mempertimbangkan untuk menggunakan namaiuple ? ( tautan untuk python 2.4 / 2.5 )

Ini adalah cara standar baru untuk merepresentasikan data terstruktur yang memberi Anda performa tupel dan kenyamanan kelas.

Satu-satunya kelemahan dibandingkan dengan kamus adalah bahwa (seperti tupel) tidak memberi Anda kemampuan untuk mengubah atribut setelah dibuat.

John Fouhy
sumber
6

Ini adalah salinan jawaban @hughdbrown untuk python 3.6.1, saya telah membuat hitungan 5x lebih besar dan menambahkan beberapa kode untuk menguji jejak memori dari proses python di akhir setiap proses.

Sebelum downvoters melakukannya, Harap diperhatikan bahwa metode penghitungan ukuran objek ini tidak akurat.

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

Dan inilah hasil saya

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

Kesimpulan saya adalah:

  1. Slot memiliki jejak memori terbaik dan kecepatannya masuk akal.
  2. penis adalah yang tercepat, tetapi menggunakan sebagian besar memori.
Jarrod Chesney
sumber
Sobat, kamu harus mengubah ini menjadi pertanyaan. Saya menjalankannya di komputer saya juga, hanya untuk memastikan (saya tidak menginstal psutil, jadi saya mengeluarkan bagian itu). Bagaimanapun, ini membingungkan saya, dan berarti pertanyaan awal belum sepenuhnya terjawab. Semua jawaban lainnya seperti "namedtuple is great" dan "use slots ", dan tampaknya objek dict baru setiap saat lebih cepat dari mereka? Saya kira penis benar-benar dioptimalkan dengan baik?
Multihunter
1
Tampaknya ini adalah hasil dari fungsi makeL yang mengembalikan string. Jika Anda mengembalikan daftar kosong, sebaliknya, hasilnya kira-kira cocok dengan hughdbrown's dari python2. Kecuali nametuple selalu lebih lambat dari SlotObj :(
Multihunter
Mungkin ada masalah kecil: makeL dapat berjalan dengan kecepatan berbeda di setiap putaran '@timeit' karena string di-cache dengan python - tapi mungkin saya salah.
Barney
@BarnabasSzabolcs harus membuat string baru setiap saat karena harus mengganti nilai "Ini adalah contoh string% s"% i
Jarrod Chesney
Ya, itu benar dalam loop, tetapi pada tes kedua saya mulai dari 0 lagi.
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()

Hasil:

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
hughdbrown
sumber
3

Tidak ada pertanyaan.
Anda memiliki data, tanpa atribut lain (tanpa metode, tidak ada). Karenanya Anda memiliki wadah data (dalam hal ini, kamus).

Saya biasanya lebih suka berpikir dalam hal pemodelan data . Jika ada masalah kinerja yang sangat besar, maka saya dapat menyerahkan sesuatu dalam abstraksi, tetapi hanya dengan alasan yang sangat bagus.
Pemrograman adalah tentang mengelola kompleksitas, dan mempertahankan abstraksi yang benar sering kali merupakan salah satu cara paling berguna untuk mencapai hasil tersebut.

Tentang alasan benda lebih lambat, saya pikir pengukuran Anda tidak benar.
Anda melakukan tugas yang terlalu sedikit di dalam perulangan for, dan oleh karena itu yang Anda lihat adalah waktu berbeda yang diperlukan untuk membuat instance dict (objek intrinsik) dan objek "kustom". Meskipun dari segi bahasa keduanya sama, namun penerapannya cukup berbeda.
Setelah itu, waktu penugasan harus hampir sama untuk keduanya, karena pada akhirnya anggota dipertahankan di dalam kamus.

rampok
sumber
0

Ada cara lain untuk mengurangi penggunaan memori jika struktur data tidak seharusnya berisi siklus referensi.

Mari bandingkan dua kelas:

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

dan

$ 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

Ini menjadi mungkin karena structclasskelas berbasis tidak mendukung pengumpulan sampah siklik, yang tidak diperlukan dalam kasus seperti itu.

Ada juga satu keuntungan dari __slots__kelas berbasis berlebih : Anda dapat menambahkan atribut tambahan:

>>> 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
intellimath
sumber
0

Berikut adalah pengujian saya atas skrip yang sangat bagus dari @ Jarrod-Chesney. Sebagai perbandingan, saya juga menjalankannya terhadap python2 dengan "range" diganti dengan "xrange".

Karena penasaran, saya juga menambahkan tes serupa dengan OrderedDict (ordict) untuk perbandingan.

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

Jadi, pada kedua versi utama, kesimpulan dari @ Jarrod-Chesney masih terlihat bagus.

Florent V
sumber