Dictionary vs Object - was ist effizienter und warum?


126

Was ist in Python in Bezug auf Speichernutzung und CPU-Verbrauch effizienter - Wörterbuch oder Objekt?

Hintergrund: Ich muss eine große Datenmenge in Python laden. Ich habe ein Objekt erstellt, das nur ein Feldcontainer ist. Das Erstellen von 4M-Instanzen und das Einfügen in ein Wörterbuch dauerte etwa 10 Minuten und ~ 6 GB Speicher. Nachdem das Wörterbuch fertig ist, ist der Zugriff darauf ein Wimpernschlag.

Beispiel: Um die Leistung zu überprüfen, habe ich zwei einfache Programme geschrieben, die dasselbe tun - eines verwendet Objekte, das andere Wörterbuch:

Objekt (Ausführungszeit ~ 18sec):

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

Wörterbuch (Ausführungszeit ~ 12sec):

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

Frage: Mache ich etwas falsch oder ist das Wörterbuch nur schneller als das Objekt? Wenn das Wörterbuch tatsächlich eine bessere Leistung erbringt, kann jemand erklären, warum?


10
Sie sollten wirklich xrange anstelle von range verwenden, wenn Sie große Sequenzen wie diese generieren. Da es sich um Sekunden Ausführungszeit handelt, macht es natürlich keinen großen Unterschied, aber es ist trotzdem eine gute Angewohnheit.
Xiong Chiamiov

2
es sei denn, es ist Python3
Barney

Antworten:


157

Haben Sie versucht, zu verwenden __slots__?

Aus der Dokumentation :

Standardmäßig verfügen Instanzen alter und neuer Klassen über ein Wörterbuch für die Attributspeicherung. Dies verschwendet Platz für Objekte mit sehr wenigen Instanzvariablen. Der Platzverbrauch kann akut werden, wenn eine große Anzahl von Instanzen erstellt wird.

Die Standardeinstellung kann durch Definieren __slots__in einer Klassendefinition neuen Stils überschrieben werden . Die __slots__Deklaration verwendet eine Folge von Instanzvariablen und reserviert in jeder Instanz gerade genug Speicherplatz, um einen Wert für jede Variable zu speichern. Speicherplatz wird gespart, da __dict__nicht für jede Instanz erstellt wird.

Spart dies also Zeit und Speicher?

Vergleich der drei Ansätze auf meinem Computer:

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 (unterstützt in 2.6):

import collections

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

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

Führen Sie einen Benchmark aus (mit 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__)

Verwenden von CPython 2.6.2, einschließlich des genannten Tupeltests:

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

Also ja (nicht wirklich überraschend), die Verwendung __slots__ist eine Leistungsoptimierung. Die Verwendung eines benannten Tupels hat eine ähnliche Leistung wie __slots__.


2
Das ist toll - danke! Ich habe dasselbe auf meinem Computer versucht - Objekt mit Steckplätzen ist der effizienteste Ansatz (ich habe ~ 7 Sekunden).
Tkokoszka

6
Es gibt auch benannte Tupel, docs.python.org/library/collections.html#collections.namedtuple , eine Klassenfactory für Objekte mit Slots. Es ist definitiv ordentlicher und vielleicht sogar noch optimierter.
Jochen Ritzel

Ich habe auch benannte Tupel getestet und die Antwort mit den Ergebnissen aktualisiert.
Codeape

1
Ich habe Ihren Code einige Male ausgeführt und war überrascht, dass meine Ergebnisse unterschiedlich sind - Slots = 3 Sek. Obj = 11 Sek. Dikt = 12 Sek. Namedtuple = 16 Sek. Ich benutze CPython 2.6.6 auf Win7 64bit
Jonathan

Um die Pointe zu betonen - NamedTuple erzielte die schlechtesten Ergebnisse anstelle der besten
Jonathan

15

Der Attributzugriff in einem Objekt verwendet den Wörterbuchzugriff hinter den Kulissen. Durch die Verwendung des Attributzugriffs erhöhen Sie den Overhead. Außerdem entsteht im Objektfall ein zusätzlicher Overhead, z. B. aufgrund zusätzlicher Speicherzuweisungen und Codeausführung (z. B. der __init__Methode).

Wenn oes sich bei Ihrem Code um eine ObjInstanz handelt, o.attrentspricht dies o.__dict__['attr']einem geringen zusätzlichen Aufwand.


Hast du das getestet? o.__dict__["attr"]ist derjenige mit zusätzlichem Overhead, der einen zusätzlichen Bytecode op benötigt; obj.attr ist schneller. (Natürlich wird der Attributzugriff nicht langsamer sein als der Abonnementzugriff - es ist ein kritischer, stark optimierter Codepfad.)
Glenn Maynard

2
Natürlich , wenn Sie tatsächlich tun o .__ dict __ [ „attr“] es wird langsamer sein - ich will nur sagen , dass es zu , dass gleichwertig war, nicht , dass es genau auf diese Weise umgesetzt wurde. Ich denke, es ist nicht klar aus meinem Wortlaut. Ich erwähnte auch andere Faktoren wie Speicherzuweisungen, Konstruktoraufrufzeit usw.
Vinay Sajip

Ist dies auch bei neueren Versionen von Python3 11 Jahre später noch der Fall?
Matanster

9

Haben Sie darüber nachgedacht, ein benanntes Tupel zu verwenden ? ( Link für Python 2.4 / 2.5 )

Dies ist die neue Standardmethode zur Darstellung strukturierter Daten, mit der Sie die Leistung eines Tupels und die Bequemlichkeit einer Klasse erzielen.

Der einzige Nachteil im Vergleich zu Wörterbüchern ist, dass Sie (wie bei Tupeln) nach der Erstellung keine Möglichkeit haben, Attribute zu ändern.


5

Hier ist eine Kopie der @ hughdbrown-Antwort für Python 3.6.1. Ich habe die Anzahl um das Fünffache erhöht und Code hinzugefügt, um den Speicherbedarf des Python-Prozesses am Ende jedes Laufs zu testen.

Bevor die Downvoter es tun, sollten Sie darauf hinweisen, dass diese Methode zum Zählen der Größe von Objekten nicht genau ist.

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

Und das sind meine Ergebnisse

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

Mein Fazit lautet:

  1. Steckplätze haben den besten Speicherbedarf und sind in Bezug auf die Geschwindigkeit angemessen.
  2. Diktate sind am schnellsten, verbrauchen aber am meisten Speicher.

Mann, du solltest das in eine Frage verwandeln. Ich habe es auch auf meinem eigenen Computer ausgeführt, nur um sicherzugehen (ich hatte psutil nicht installiert, also habe ich diesen Teil herausgenommen). Auf jeden Fall ist das für mich verwirrend und bedeutet, dass die ursprüngliche Frage nicht vollständig beantwortet ist. Alle anderen Antworten lauten wie "namedtuple is great" und "use slots ", und anscheinend ist jedes Mal ein brandneues Diktierobjekt schneller als sie? Ich denke, Diktate sind wirklich gut optimiert?
Multihunter

1
Es scheint das Ergebnis der makeL-Funktion zu sein, die einen String zurückgibt. Wenn Sie stattdessen eine leere Liste zurückgeben, stimmen die Ergebnisse in etwa mit denen von hughdbrown aus python2 überein. Außer Namedtuples sind immer langsamer als SlotObj :(
Multihunter

Es könnte ein kleines Problem geben: makeL könnte in jeder '@timeit'-Runde mit unterschiedlichen Geschwindigkeiten ausgeführt werden, da Zeichenfolgen in Python zwischengespeichert werden - aber vielleicht irre ich mich.
Barney

@BarnabasSzabolcs sollte jedes Mal eine neue Zeichenfolge erstellen, da diese den Wert "Dies ist eine Beispielzeichenfolge% s" ersetzen muss.% I
Jarrod Chesney

Ja, das stimmt innerhalb der Schleife, aber im zweiten Test beginne ich wieder bei 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()

Ergebnisse:

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

Es gibt keine Frage.
Sie haben Daten ohne andere Attribute (keine Methoden, nichts). Sie haben also einen Datencontainer (in diesem Fall ein Wörterbuch).

Normalerweise denke ich lieber in Datenmodellierung . Wenn es ein großes Leistungsproblem gibt, kann ich etwas in der Abstraktion aufgeben, aber nur mit sehr guten Gründen.
Bei der Programmierung geht es darum, die Komplexität zu verwalten, und die Aufrechterhaltung der richtigen Abstraktion ist sehr oft einer der nützlichsten Wege, um ein solches Ergebnis zu erzielen.

Über die Gründe, warum ein Objekt langsamer ist, denke ich, dass Ihre Messung nicht korrekt ist.
Sie führen zu wenig Zuweisungen innerhalb der for-Schleife aus, und daher sehen Sie dort die unterschiedliche Zeit, die erforderlich ist, um ein Diktat (intrinsisches Objekt) und ein "benutzerdefiniertes" Objekt zu instanziieren. Obwohl sie aus sprachlicher Sicht gleich sind, haben sie eine ganz andere Implementierung.
Danach sollte die Zuweisungszeit für beide nahezu gleich sein, da die Mitglieder am Ende in einem Wörterbuch verwaltet werden.


0

Es gibt noch eine andere Möglichkeit, die Speichernutzung zu reduzieren, wenn die Datenstruktur keine Referenzzyklen enthalten soll.

Vergleichen wir zwei Klassen:

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

und

$ 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

Dies wurde möglich, da structclass-basierte Klassen die zyklische Speicherbereinigung nicht unterstützen, was in solchen Fällen nicht erforderlich ist.

Es gibt auch einen Vorteil gegenüber einer __slots__Klasse auf Basis: Sie können zusätzliche Attribute hinzufügen:

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

Hier sind meine Testläufe des sehr schönen Skripts von @ Jarrod-Chesney. Zum Vergleich führe ich es auch gegen Python2 aus, wobei "range" durch "xrange" ersetzt wird.

Aus Neugier habe ich zum Vergleich auch ähnliche Tests mit OrderedDict (ordict) hinzugefügt.

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

In beiden Hauptversionen sehen die Schlussfolgerungen von @ Jarrod-Chesney immer noch gut aus.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.