2009-08-26 31 views
91

¿Qué es más eficiente en Python en términos de uso de memoria y consumo de CPU? ¿Diccionario u objeto?Diccionario vs Objeto: ¿qué es más eficiente y por qué?

Antecedentes: Tengo que cargar gran cantidad de datos en Python. Creé un objeto que es solo un contenedor de campo. Crear instancias de 4M y ponerlas en un diccionario tomó alrededor de 10 minutos y ~ 6GB de memoria. Después de que el diccionario esté listo, acceder a él es un abrir y cerrar de ojos.

Ejemplo: para comprobar el rendimiento escribí dos programas sencillos que hacen lo mismo - una es el uso de objetos, otro diccionario:

objeto (tiempo de ejecución ~ 18sec):

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

Diccionario (tiempo de ejecución ~ 12seg):

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

pregunta: ¿Estoy haciendo algo mal o el diccionario es más rápido que el objeto? Si el diccionario funciona mejor, ¿alguien puede explicar por qué?

+9

Realmente debería usar xrange en lugar de rango al generar secuencias de gran tamaño como esa. Por supuesto, ya que estás tratando con segundos de tiempo de ejecución, no habrá mucha diferencia, pero aun así, es un buen hábito. –

Respuesta

129

¿Ha intentado utilizar __slots__?

De la documentación http://docs.python.org/reference/datamodel.html#slots:

"Por defecto, los casos de los viejos y de nuevo cuño clases tienen un diccionario para el almacenamiento atributo Este espacio de desechos para objetos que tienen muy pocas variables de instancia El consumo de espacio es muy agudo.. al crear un gran número de instancias.

El valor predeterminado puede anularse definiendo __slots__ en una definición de clase de estilo nuevo. La declaración __slots__ toma una secuencia de variables de instancia y reserva suficiente espacio en cada instancia para contener un valor para cada variable. Se guarda espacio porque __dict__ no se crea para cada instancia ".

¿Esto ahorra tiempo y memoria?

Comparando los tres enfoques en mi ordenador:

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 (apoyado en 2.6):

import collections 

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

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

Run punto de referencia (usando 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__) 

Usando CPython 2.6.2, incluyendo la tupla llamado prueba:

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

Así que sí (no realmente una sorpresa), usando __slots__ es una optimización del rendimiento. El uso de una tupla con nombre tiene un rendimiento similar al __slots__.

+1

Eso es genial, ¡gracias! He intentado lo mismo en mi máquina: el objeto con __slots__ es el enfoque más eficiente (tengo ~ 7sec). – tkokoszka

+5

También se nombran tuplas, http://docs.python.org/library/collections.html#collections.namedtuple, una fábrica de clases para objetos con ranuras. Definitivamente más ordenado y tal vez incluso más optimizado. –

+0

También probé tuplas con nombre y actualicé la respuesta con los resultados. – codeape

11

El acceso de atributo en un objeto utiliza acceso de diccionario detrás de escena, por lo que al usar el acceso de atributo está agregando sobrecarga adicional. Además, en el caso del objeto, está incurriendo en gastos adicionales debido a, p. Ej. asignaciones de memoria adicionales y ejecución de código (por ejemplo, del método __init__).

En su código, si o es una instancia Obj, o.attr es equivalente a o.__dict__['attr'] con una pequeña cantidad de sobrecarga adicional.

+0

¿Eh? ¿Por qué downvote? –

+0

¿Has probado esto? 'o .__ dict __ [" attr "]' es el que tiene una carga adicional, tomando una operación adicional de bytecode; obj.attr es más rápido. (Por supuesto, el acceso al atributo no va a ser más lento que el acceso de suscripción; es una ruta de código crítica y muy optimizada.) –

+0

Obviamente, si en realidad ** do ** o .__ dict __ ["attr"] será más lento - Solo quise decir que era equivalente a eso, no que se implementara exactamente de esa manera. Supongo que no está claro en mi redacción. También mencioné otros factores como las asignaciones de memoria, el tiempo de llamada del constructor, etc. –

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

Resultados:

[email protected]:~$ 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 
2

No hay duda.
Tiene datos, sin otros atributos (sin métodos, nada). Por lo tanto, tiene un contenedor de datos (en este caso, un diccionario).

Por lo general, prefiero pensar en términos de modelado de datos. Si hay un gran problema de rendimiento, entonces puedo renunciar a algo en la abstracción, pero solo con muy buenas razones.
La programación tiene que ver con la gestión de la complejidad, y el mantenimiento de la abstracción correcta es a menudo una de las formas más útiles para lograr dicho resultado.

Acerca de motivos un objeto es más lento, creo que su medida no es correcta.
Está realizando asignaciones demasiado pequeñas dentro del bucle for, y por lo tanto lo que ve allí es el tiempo diferente necesario para crear una instancia de un dict (objeto intrínseco) y un objeto "personalizado". Aunque desde la perspectiva del lenguaje son los mismos, tienen una implementación bastante diferente.
Después de eso, el tiempo de asignación debe ser casi el mismo para ambos, ya que al final los miembros se mantienen dentro de un diccionario.

7

¿Ha considerado usar un namedtuple? (link for python 2.4/2.5)

Es la nueva forma estándar de representar datos estructurados que le ofrece el rendimiento de una tupla y la comodidad de una clase.

Su único inconveniente en comparación con los diccionarios es que (como las tuplas) no le da la capacidad de cambiar los atributos después de la creación.

+0

@Eloims Disculpe, formulación poco clara. Lo he arreglado –

2

Aquí hay una copia de @hughdbrown answer para python 3.6.1, hice que el conteo 5 veces más grande y agregué un código para probar la huella de memoria del proceso de python al final de cada ejecución.

Antes de que los downvoters lo tengan, tenga en cuenta que este método de contar el tamaño de los objetos no es preciso.

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

y estos son mis resultados

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 

Mi conclusión es:

  1. ranuras tienen la mejor capacidad de memoria y son razonables en la velocidad.
  2. Los dicts son los más rápidos, pero usan la mayor cantidad de memoria.
+0

Hombre, deberías convertir esto en una pregunta. Lo ejecuté en mi propia computadora también, solo para asegurarme (no tenía instalado el psutil, así que eliminé esa parte). De todos modos, esto es desconcertante para mí, y significa que la pregunta original no está completamente respondida. Todas las otras respuestas son como "namedtuple is great" y "use __slots__", y aparentemente un nuevo objeto dict cada vez es más rápido que ellos. Supongo que los dicts están realmente bien optimizados. – Multihunter

+0

Parece ser el resultado de que la función makeL devuelve una cadena. Si devuelve una lista vacía, en su lugar, los resultados coinciden aproximadamente con hughdbrown's de python2. Excepto namedtuples siempre son más lentos que SlotObj :( – Multihunter

Cuestiones relacionadas