Quoi de plus efficace en Python en termes d’utilisation de mémoire et de consommation d’UC - Dictionnaire ou Objet?
Background: Je dois charger une énorme quantité de données dans Python. J'ai créé un objet qui n'est qu'un conteneur de champ. Créer des instances 4M et les mettre dans un dictionnaire prenait environ 10 minutes et environ 6 Go de mémoire. Une fois que le dictionnaire est prêt, il est facile d'y accéder.
Exemple: Pour vérifier les performances, j’ai écrit deux programmes simples qui font la même chose - l’un utilise des objets, l’autre dictionnaire:
Objet (temps d'exécution ~ 18sec):
class Obj(object):
def __init__(self, i):
self.i = i
self.l = []
all = {}
for i in range(1000000):
all[i] = Obj(i)
Dictionnaire (temps d'exécution ~ 12sec):
all = {}
for i in range(1000000):
o = {}
o['i'] = i
o['l'] = []
all[i] = o
Question: Est-ce que je fais quelque chose de mal ou le dictionnaire est juste plus rapide que l'objet? Si le dictionnaire fonctionne mieux, quelqu'un peut-il expliquer pourquoi?
Avez-vous essayé d'utiliser __slots__
?
De la documentation :
Par défaut, les instances des classes de style ancien et nouveau ont un dictionnaire pour le stockage d'attributs. Cela gaspille de l'espace pour des objets ayant très peu de variables d'instance. La consommation d'espace peut devenir aiguë lors de la création d'un grand nombre d'instances.
La valeur par défaut peut être remplacée en définissant
__slots__
dans une définition de classe d'un nouveau style. La déclaration__slots__
prend une séquence de variables d'instance et réserve juste assez d'espace dans chaque instance pour contenir une valeur pour chaque variable. L'espace est enregistré car__dict__
n'est pas créé pour chaque instance.
Alors, est-ce que cela fait gagner du temps et de la mémoire?
En comparant les trois approches sur mon ordinateur:
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 (pris en charge en 2.6):
import collections
Obj = collections.namedtuple('Obj', 'i l')
all = {}
for i in range(1000000):
all[i] = Obj(i, [])
Exécuter un benchmark (à l'aide de 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__)
Avec CPython 2.6.2, y compris le test de tuple nommé:
$ 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)
Donc oui (pas vraiment une surprise), utiliser __slots__
est une optimisation des performances. L'utilisation d'un tuple nommé a des performances similaires à __slots__
.
L'accès aux attributs dans un objet utilise l'accès au dictionnaire en coulisse. Ainsi, en utilisant l'accès aux attributs, vous ajoutez une surcharge supplémentaire. De plus, dans le cas des objets, vous induisez des frais généraux supplémentaires, par exemple des allocations de mémoire supplémentaires et l'exécution de code (par exemple, de la méthode __init__
).
Dans votre code, si o
est une instance Obj
, o.attr
équivaut à o.__dict__['attr']
avec une petite quantité de temps système supplémentaire.
Avez-vous envisagé d'utiliser un namedtuple ? ( lien pour python 2.4/2.5 )
C'est la nouvelle manière standard de représenter des données structurées qui vous donne les performances d'un tuple et la commodité d'une classe.
Le seul inconvénient, comparé aux dictionnaires, est que (comme les tuples), cela ne vous donne pas la possibilité de changer les attributs après la création.
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 __== '__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()
Résultats:
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
Voici une copie de @hughdbrown answer pour python 3.6.1. J'ai multiplié le nombre par cinq et ajouté du code pour tester l'empreinte mémoire du processus python à la fin de chaque exécution.
Avant que les électeurs n’arrivent à la fin, sachez que cette méthode de comptage de la taille des objets n’est pas précise.
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()
Et ce sont mes résultats
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
Ma conclusion est:
Il n'y a aucune question.
Vous avez des données, sans autres attributs (pas de méthodes, rien). Vous avez donc un conteneur de données (dans ce cas, un dictionnaire).
Je préfère généralement penser en termes de modélisation de données . S'il y a un problème de performance énorme, je peux alors renoncer à quelque chose dans l'abstraction, mais seulement avec de très bonnes raisons.
La programmation consiste uniquement à gérer la complexité et le maintien de la abstraction correcte est très souvent l’un des moyens les plus utiles d’atteindre ce résultat.
À propos des raisons un objet est plus lent, je pense que votre mesure est incorrecte.
Vous effectuez trop peu d’assignations dans la boucle for et vous voyez donc le temps nécessaire pour instancier un dict (objet intrinsèque) et un objet "personnalisé". Bien que, du point de vue linguistique, ils soient identiques, leur mise en œuvre est très différente.
Après cela, le temps d’attribution devrait être presque le même pour les deux, car à la fin, les membres sont conservés dans un dictionnaire.
Il existe un autre moyen de réduire l'utilisation de la mémoire si la structure de données n'est pas censée contenir des cycles de référence.
Comparons deux classes:
class DataItem:
__slots__ = ('name', 'age', 'address')
def __init__(self, name, age, address):
self.name = name
self.age = age
self.address = address
et
$ 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
Cela est devenu possible car les classes basées sur structclass
ne prennent pas en charge la récupération de place cyclique, ce qui n'est pas nécessaire dans de tels cas.
Il existe également un avantage par rapport à la classe basée sur __slots__
: vous pouvez ajouter des attributs supplémentaires:
>>> 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