Подтвердить что ты не робот

Сохранение большого словаря в Python влияет на производительность приложения

У меня возникают трудности с пониманием (и в конечном итоге решением), почему наличие большого словаря в памяти делает создание других словарей дольше.

Здесь тестовый код, который я использую

import time
def create_dict():
    # return {x:[x]*125 for x in xrange(0, 100000)}
    return  {x:(x)*125 for x in xrange(0, 100000)}  # UPDATED: to use tuples instead of list of values


class Foo(object):
    @staticmethod
    def dict_init():
        start = time.clock()
        Foo.sample_dict = create_dict()
        print "dict_init in Foo took {0} sec".format(time.clock() - start)

if __name__ == '__main__':
    Foo.dict_init()
    for x in xrange(0, 10):
        start = time.clock()
        create_dict()
        print "Run {0} took {1} seconds".format(x, time.clock() - start)

Если я запускаю код как есть (сначала инициализируя sample_dict в Foo), а затем создавая тот же словарь еще 10 раз в цикле, я получаю следующие результаты:

dict_init in Foo took 0.385263764287 sec
Run 0 took 0.548807949139 seconds
Run 1 took 0.533209452471 seconds
Run 2 took 0.51916067636 seconds
Run 3 took 0.513130722575 seconds
Run 4 took 0.508272050029 seconds
Run 5 took 0.502263872177 seconds
Run 6 took 0.48867601998 seconds
Run 7 took 0.483109299676 seconds
Run 8 took 0.479019713488 seconds
Run 9 took 0.473174195256 seconds
[Finished in 5.6s]

Если, однако, я НЕ инициализирую sample_dict в Foo (комментируя Foo.dict_init()) Я получаю почти на 20% быстрее создание словаря в цикле

Run 0 took 0.431378921359 seconds
Run 1 took 0.423696636179 seconds
Run 2 took 0.419630475616 seconds
Run 3 took 0.405130343806 seconds
Run 4 took 0.398099686921 seconds
Run 5 took 0.392837169802 seconds
Run 6 took 0.38799598399 seconds
Run 7 took 0.375133006408 seconds
Run 8 took 0.368755297573 seconds
Run 9 took 0.363273701371 seconds
[Finished in 4.0s]

Я заметил, что если я выключу сборщик мусора Python, вызывая gc.disable(), производительность не только улучшает ~ 5x, но и хранение большого словаря в Foo не имеет значения. Ниже приведены результаты с отключенной сборкой мусора:

dict_init in Foo took 0.0696136982496 sec
Run 0 took 0.113533445358 seconds
Run 1 took 0.111091241489 seconds
Run 2 took 0.111151620212 seconds
Run 3 took 0.110655722831 seconds
Run 4 took 0.111807537706 seconds
Run 5 took 0.11097510318 seconds
Run 6 took 0.110936170451 seconds
Run 7 took 0.111074414632 seconds
Run 8 took 0.110678488579 seconds
Run 9 took 0.111011066463 seconds

У меня есть 2 вопроса:

  • Почему отключение сборки мусора ускоряет создание словаря.
  • Как достичь даже производительности (с помощью Foo init и без него) без отключения сборки мусора.

Буду признателен за это.

Спасибо!

ОБНОВЛЕНО: После того, как Тим Петерс упомянул, что я создаю изменяемые объекты, я решил попытаться создать неизменяемые объекты (кортежи в моем случае) и voila - гораздо более быстрые результаты (то же самое с использованием gc и без)

dict_init in Foo took 0.017769 sec
Run 0 took 0.017547 seconds
Run 1 took 0.013234 seconds
Run 2 took 0.012791 seconds
Run 3 took 0.013371 seconds
Run 4 took 0.013288 seconds
Run 5 took 0.013692 seconds
Run 6 took 0.013059 seconds
Run 7 took 0.013311 seconds
Run 8 took 0.013343 seconds
Run 9 took 0.013675 seconds

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

Спасибо.

P.S. Как это бывает, в моем реальном сценарии преобразование списка в кортежи разрешило проблему (никогда не было необходимости иметь списки, просто не думал об использовании кортежей), но мне все еще интересно, почему это быстрее.

4b9b3361

Ответ 1

"Создание словаря" на самом деле - красная селедка. То, что делает словарь в этом случае, имеет важное значение в том, что он создает сто тысяч новых 125-элементных списков. Поскольку списки могут быть задействованы в эталонных циклах, что создает 12,5 миллионов элементов списка, циклическая сборка мусора CPython должна проверять каждый раз, когда она сканирует поколение, содержащее dict. Не имеет значения, что эти списки находятся в словарях, и только важно, чтобы они существовали.

Итак, вы выбрали время, затраченное на циклическую сборку мусора Python. Не имеет особого значения, что вы продолжаете создавать больше dicts, важно только, чтобы вы создавали новые изменяемые объекты (которые могут быть задействованы в циклах) намного быстрее, чем вы уничтожаете старые изменчивые объекты. Это (избыток распределений по деаллокациям) является тем, что запускает CPython циклический gc).

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

А, забыл одно: dict в Foo делает такое большое различие, потому что Foo "прилипает". Все остальные создаваемые вами dicts выбрасываются сразу после их создания (за это отвечает счет ссылок на CPython), поэтому не добавляйте к времени, затрачиваемому циклическим gc. Но dict в Foo делает, потому что этот dict не уходит. Измените свой цикл, чтобы добавить новые dicts в список, и вы увидите, что время увеличивается для каждого dict, затем падает много, затем снова поднимается и т.д. Это отражает, что dicts переходит к более старым поколениям внутри циклического gc Python, так что часто проверяются циклическим gc. Это осложняется: -)

Подробнее?

Трудно быть более точным, так как производительность циклического gc в определенных случаях зависит от гор деталей реализации, которые могут - и делать - изменять разные версии.

Общие рекомендации по использованию "неизменяемых объектов", когда это возможно, основаны на довольно глубоком;-) понимании того, как циклический gc реализуется в CPython и как он эволюционировал с годами.

Так получилось, что сегодня (самые последние версии Python 2 и Python 3) прилагаются сильные усилия, чтобы освободить определенные кортежи и dicts от циклического gc. Это может измениться. Специальная оболочка таких вещей не является бесплатной, поэтому решение о том, добавлять ли дополнительные трюки, как это, всегда является сложным балансирующим действием. Это более легкое решение для неизменных объектов, следовательно, совет двигаться к ним.

Кортежи и диктофоны не были специально обрезаны до конца 2008 года, как описано в в этом из трекера Python.

И - удивление;-) - оказалось, что в некоторых редких случаях были ужасные последствия для производительности, которые были исправлены более специальным корпусом в этом выпуске в середине 2012.

Хорошей новостью является то, что была добавлена ​​функция gc.is_tracked(), поэтому вы можете, по крайней мере, исследовать, будет ли циклический gc сканировать определенный объект. Вот некоторые примеры в Python 2.7.5. Там нет гарантии, что они всегда будут работать таким образом:

"Скалярные" объекты (без внутренних указателей) никогда не отслеживаются - для них невозможно быть в цикле:

>>> import gc
>>> gc.is_tracked(4)
False
>>> gc.is_tracked("2323")
False

Первоначально отслеживаются кортежи:

>>> t1 = ([1],)
>>> t2 = ((1.),)
>>> gc.is_tracked(t1), gc.is_tracked(t2)
(True, True)

Однако, если циклический gc работает и определяет, что кортеж неизменен "полностью вниз", он не проверяет этот кортеж:

>>> gc.collect()
0
>>> gc.is_tracked(t1), gc.is_tracked(t2)
(True, False)

Нет ничего, что можно сделать для t2, чтобы он участвовал в цикле, потому что он и все его компоненты (все включено и выключены) неизменны. Но t1 еще нужно отслеживать! Поскольку t1[0] является изменяемым, t1 может быть частью цикла позже:

>>> t1
([1],)
>>> t1[0][0] = t1
>>> t1
([([...],)],)

Для dicts используется другая политика. Они создаются без следа, если это возможно:

>>> d = {1: [2]}
>>> gc.is_tracked(d)
True

Поскольку этот dict имеет изменяемое значение, он может стать частью цикла позже, поэтому его нужно отслеживать циклическим gc.

>>> d[1][0] = d
>>> d
{1: [{...}]}

Но dict с неизведанными ключами и значениями создается без следа:

>>> d = {1: 2}
>>> gc.is_tracked(d)
False

Это сложно, потому что такой дикт еще может стать частью цикла позже!

>>> d[2] = d
>>> gc.is_tracked(d)
True

Невозможно обнаружить такие изменения.

Тогда есть комбинации выше:

>>> d = {(1, 2): (4, "abc", 5)}
>>> gc.is_tracked(d)
True
>>> gc.collect()
3
>>> gc.is_tracked(d)
False

В этом случае сначала отслеживается d, потому что сначала его отслеживают его ключи и значения (кортежи). Но после того, как циклический gc запускается в первый раз, он определяет, что ключи и значения являются "неизменными вплоть до конца", поэтому не проверяет dict.

Как я уже сказал, это осложняется: -)

Кстати,

Я понимаю, что создание кортежей намного быстрее, чем создание списка

Для создания списка должно быть немного медленнее. Списки и кортежи имеют очень похожие реализации в CPython. кортежи требуют немного меньше памяти (что может быть значительным, в процентном отношении, для очень коротких последовательностей), и может быть немного быстрее индексировать кортеж, чем список. Но время творения? Это различие между одной malloc() -подобной функцией (для кортежа) по сравнению с двумя (для списка), независимо от количества элементов. Это может быть значительным для очень коротких последовательностей, но не для больших.

Ответ 2

Измените эту программу, чтобы проверить байт-код:

import time
import dis
import inspect

def create_dict():
    return {x:[x]*125 for x in xrange(0, 100000)}


class Foo(object):
    @staticmethod
    def dict_init():
        start = time.clock()
        Foo.sample_dict = create_dict()
        print "dict_init in Foo took {0} sec".format(time.clock() - start)
        dis.dis(inspect.currentframe().f_code)

if __name__ == '__main__':
    Foo.dict_init()
    for x in xrange(0, 1):
        start = time.clock()
        create_dict()
        print "Run {0} took {1} seconds".format(x, time.clock() - start)
        dis.dis(inspect.currentframe().f_code)

Вот результат:

dict_init in Foo took 0.44164 sec
 12           0 LOAD_GLOBAL              0 (time)
              3 LOAD_ATTR                1 (clock)
              6 CALL_FUNCTION            0
              9 STORE_FAST               0 (start)

 13          12 LOAD_GLOBAL              2 (create_dict)
             15 CALL_FUNCTION            0
             18 LOAD_GLOBAL              3 (Foo)
             21 STORE_ATTR               4 (sample_dict)

 14          24 LOAD_CONST               1 ('dict_init in Foo took {0} sec')
             27 LOAD_ATTR                5 (format)
             30 LOAD_GLOBAL              0 (time)
             33 LOAD_ATTR                1 (clock)
             36 CALL_FUNCTION            0
             39 LOAD_FAST                0 (start)
             42 BINARY_SUBTRACT     
             43 CALL_FUNCTION            1
             46 PRINT_ITEM          
             47 PRINT_NEWLINE       

 15          48 LOAD_GLOBAL              6 (dis)
             51 LOAD_ATTR                6 (dis)
             54 LOAD_GLOBAL              7 (inspect)
             57 LOAD_ATTR                8 (currentframe)
             60 CALL_FUNCTION            0
             63 LOAD_ATTR                9 (f_code)
             66 CALL_FUNCTION            1
             69 POP_TOP             
             70 LOAD_CONST               0 (None)
             73 RETURN_VALUE        
Run 0 took 0.641144 seconds
  1           0 LOAD_CONST               0 (-1)
              3 LOAD_CONST               1 (None)
              6 IMPORT_NAME              0 (time)
              9 STORE_NAME               0 (time)

  2          12 LOAD_CONST               0 (-1)
             15 LOAD_CONST               1 (None)
             18 IMPORT_NAME              1 (dis)
             21 STORE_NAME               1 (dis)

  3          24 LOAD_CONST               0 (-1)
             27 LOAD_CONST               1 (None)
             30 IMPORT_NAME              2 (inspect)
             33 STORE_NAME               2 (inspect)

  5          36 LOAD_CONST               2 (<code object create_dict at 0x1091396b0, file "dict.py", line 5>)
             39 MAKE_FUNCTION            0
             42 STORE_NAME               3 (create_dict)

  9          45 LOAD_CONST               3 ('Foo')
             48 LOAD_NAME                4 (object)
             51 BUILD_TUPLE              1
             54 LOAD_CONST               4 (<code object Foo at 0x10914c130, file "dict.py", line 9>)
             57 MAKE_FUNCTION            0
             60 CALL_FUNCTION            0
             63 BUILD_CLASS         
             64 STORE_NAME               5 (Foo)

 17          67 LOAD_NAME                6 (__name__)
             70 LOAD_CONST               5 ('__main__')
             73 COMPARE_OP               2 (==)
             76 POP_JUMP_IF_FALSE      186

 18          79 LOAD_NAME                5 (Foo)
             82 LOAD_ATTR                7 (dict_init)
             85 CALL_FUNCTION            0
             88 POP_TOP             

 19          89 SETUP_LOOP              94 (to 186)
             92 LOAD_NAME                8 (xrange)
             95 LOAD_CONST               6 (0)
             98 LOAD_CONST               7 (1)
            101 CALL_FUNCTION            2
            104 GET_ITER            
        >>  105 FOR_ITER                74 (to 182)
            108 STORE_NAME               9 (x)

 20         111 LOAD_NAME                0 (time)
            114 LOAD_ATTR               10 (clock)
            117 CALL_FUNCTION            0
            120 STORE_NAME              11 (start)

 21         123 LOAD_NAME                3 (create_dict)
            126 CALL_FUNCTION            0
            129 POP_TOP             

 22         130 LOAD_CONST               8 ('Run {0} took {1} seconds')
            133 LOAD_ATTR               12 (format)
            136 LOAD_NAME                9 (x)
            139 LOAD_NAME                0 (time)
            142 LOAD_ATTR               10 (clock)
            145 CALL_FUNCTION            0
            148 LOAD_NAME               11 (start)
            151 BINARY_SUBTRACT     
            152 CALL_FUNCTION            2
            155 PRINT_ITEM          
            156 PRINT_NEWLINE       

 23         157 LOAD_NAME                1 (dis)
            160 LOAD_ATTR                1 (dis)
            163 LOAD_NAME                2 (inspect)
            166 LOAD_ATTR               13 (currentframe)
            169 CALL_FUNCTION            0
            172 LOAD_ATTR               14 (f_code)
            175 CALL_FUNCTION            1
            178 POP_TOP             
            179 JUMP_ABSOLUTE          105
        >>  182 POP_BLOCK           
            183 JUMP_FORWARD             0 (to 186)
        >>  186 LOAD_CONST               1 (None)
            189 RETURN_VALUE        

Возможно, это разница в формате строки, которая вызывает разницу, когда сбор мусора отключен.