2009-08-26 2 views
91

Что более эффективно в Python с точки зрения использования памяти и потребления процессора - словарь или объект?Словарь vs Object - что более эффективно и почему?

Фон: Мне нужно загрузить огромное количество данных в Python. Я создал объект, который является полевым контейнером. Создание экземпляров 4M и помещение их в словарь занимали около 10 минут и ~ 6 ГБ памяти. После того, как словарь готов, доступ к нему - мгновенный взгляд.

Пример: Для проверки производительности я написал две простые программы, которые делают то же самое - один использует объекты, другой словарь:

объекта (время выполнения ~ 18sec):

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 

Вопрос: Я делаю что-то неправильно или словарь - это быстрее, чем объект? Если действительно словарь лучше работает, может кто-нибудь объяснить, почему?

+9

Вы должны использовать xrange вместо диапазона при создании больших последовательностей. Конечно, поскольку вы имеете дело с секундами времени выполнения, это не будет иметь большого значения, но, тем не менее, это хорошая привычка. –

ответ

129

Вы пытались использовать __slots__?

Из документации http://docs.python.org/reference/datamodel.html#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.ру:

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

+1

Это здорово - спасибо! Я пробовал то же самое на своей машине - объект с __slots__ является наиболее эффективным подходом (я получил ~ 7сек). – tkokoszka

+5

Есть также названные кортежи, http://docs.python.org/library/collections.html#collections.namedtuple, фабрика классов для объектов с слотами. Он определенно опрятный и, возможно, даже более оптимизирован. –

+0

Я также тестировал и кортежи, и обновил ответ с результатами. – codeape

11

Доступ к атрибуту в объекте использует доступ к словарю за кулисами - поэтому, используя доступ к атрибутам, вы добавляете дополнительные служебные данные. Кроме того, в объектном случае вы получаете дополнительные накладные расходы из-за, например, дополнительные выделения памяти и выполнение кода (например, метода __init__).

В вашем коде, если o является Obj экземпляр, o.attr эквивалентно o.__dict__['attr'] с небольшим количеством дополнительных накладных расходов.

+0

Э? Почему нисходящий? –

+0

Вы проверили это? 'o .__ dict __ [" attr "]' является тем, у кого есть дополнительные накладные расходы, принимая дополнительный байт-код op; obj.attr быстрее. (Конечно, доступ к атрибутам не будет медленнее, чем доступ к подписке - это критический, сильно оптимизированный путь кода.) –

+0

Очевидно, что если вы на самом деле ** делаете ** o .__ dict __ ["attr"], он будет медленнее - Я только хотел сказать, что это было эквивалентно этому, а не то, что оно было реализовано именно так. Думаю, это не ясно из моей формулировки. Я также упомянул о других факторах, таких как распределение памяти, время вызова конструктора и т. Д. –

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

Результаты:

[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

Там нет вопроса.
У вас есть данные, без каких-либо других атрибутов (без методов, ничего). Следовательно, у вас есть контейнер данных (в данном случае словарь).

Я обычно предпочитаю думать в терминах данные моделирования. Если есть какая-то огромная проблема с производительностью, я могу отказаться от чего-то в абстракции, но только с очень вескими причинами.
Программирование - это все об управлении сложностью, а поддержание правильной абстракции очень часто является одним из наиболее полезных способов достижения такого результата.

О причинах Объект медленнее, я думаю, что ваши измерения неверны.
Вы выполняете слишком мало назначений внутри цикла for, и поэтому вы видите, что существует время, необходимое для создания экземпляра dict (внутреннего объекта) и «пользовательского» объекта. Хотя с точки зрения языка они одинаковы, они имеют совершенно другую реализацию.
После этого время назначения должно быть почти одинаковым для обоих, так как в конце элементы поддерживаются внутри словаря.

7

Считаете ли вы использование namedtuple? (link for python 2.4/2.5)

Это новый стандартный способ представления структурированных данных, который дает вам производительность кортежа и удобство класса.

Это только недостаток по сравнению со словарями - это то, что (например, кортежи) он не дает вам возможности изменять атрибуты после создания.

+0

@Eloims Извините, неясная формулировка. Я исправил это. –

2

Вот копия @hughdbrown ответа для python 3.6.1, я сделал счет 5x больше и добавил некоторый код, чтобы проверить объем памяти процесса 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. dicts являются самыми быстрыми, но используют большую часть памяти.
+0

Человек, вы должны превратить это в вопрос. Я тоже запускал его на своем компьютере, чтобы убедиться (у меня не было установленного psutil, поэтому я взял эту часть). Во всяком случае, это меня озадачивает и означает, что исходный вопрос не получил полного ответа. Все остальные ответы похожи на «namedtuple is great» и «use __slots__», и, по-видимому, новый объект dict каждый раз быстрее, чем они? Думаю, дикты действительно хорошо оптимизированы? – Multihunter

+0

Кажется, результат функции makeL возвращает строку. Если вы вернете пустой список, вместо этого результаты примерно совпадают с hughdbrown's из python2. За исключением namedtuples всегда медленнее, чем SlotObj :( – Multihunter

Смежные вопросы