Как использовать подпроцессы, чтобы заставить python выпускать память?

Я читал Python Memory Management и хотел бы уменьшить объем памяти моего приложения. Было высказано предположение, что подпроцессы будут иметь большое значение для смягчения проблемы; но у меня возникли проблемы с концептуализацией того, что нужно сделать. Может ли кто-нибудь указать простой пример того, как это сделать...

def my_function():
    x = range(1000000)
    y = copy.deepcopy(x)
    del x
    return y

@subprocess_witchcraft
def my_function_dispatcher(*args):
    return my_function()

... в реальную подпроцессную функцию, которая не хранит дополнительный "свободный список"?

Бонусный вопрос:

Используется ли эта концепция "свободного списка" для c-расширений python?

Ответ 1

Важное значение в предложении оптимизации состоит в том, чтобы убедиться, что my_function() вызывается только в подпроцессе. deepcopy и del не имеют значения - как только вы создадите пять миллионов различных целых чисел в процессе, одновременно удерживая их всех, игра закончится. Даже если вы перестанете ссылаться на эти объекты, Python освободит их, сохранив ссылки на пять миллионов пустых полей размера целочисленного объекта в limbo, где они ожидают повторного использования для следующей функции, которая хочет создать пять миллионов целых чисел. Это бесплатный список, упомянутый в другом ответе, и он покупает ослепительно быстрое распределение и освобождение int и float. Для Python вполне справедливо отметить, что это не утечка памяти, так как память определенно доступна для дальнейших распределений. Тем не менее, эта память не будет возвращена системе до тех пор, пока процесс не завершится, и не будет использоваться повторно для чего-либо другого, кроме выделения номеров того же типа.

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

Здесь находится подпроцесс: если список чисел создается в другом процессе, то вся память, связанная с этим списком, включая, но не ограничиваясь, хранилище ints, освобождается и возвращается в систему простым акт прекращения подпроцесса. Конечно, вы должны разработать свою программу, чтобы список мог быть создан и обработан в подсистеме, не требуя передачи всех этих чисел. Подпроцесс может получать информацию, необходимую для создания набора данных, и может отправлять обратно информацию, полученную при обработке списка.

Чтобы проиллюстрировать этот принцип, обновите свой пример, чтобы весь список действительно существовал - скажем, мы сравниваем алгоритмы сортировки. Мы хотим создать огромный список целых чисел, отсортировать его и надежно освободить память, связанную с этим списком, чтобы следующий тест мог выделить память для собственных нужд, не беспокоясь о том, что у нее не хватает ОЗУ. Чтобы создать подпроцесс и установить связь, это использует модуль multiprocessing:

# To run this, save it to a file that looks like a valid Python module, e.g.
# "foo.py" - multiprocessing requires being able to import the main module.
# Then run it with "python foo.py".

import multiprocessing, random, sys, os, time

def create_list(size):
    # utility function for clarity - runs in subprocess
    maxint = sys.maxint
    randrange = random.randrange
    return [randrange(maxint) for i in xrange(size)]

def run_test(state):
    # this function is run in a separate process
    size = state['list_size']
    print 'creating a list with %d random elements - this can take a while... ' % size,
    sys.stdout.flush()
    lst = create_list(size)
    print 'done'
    t0 = time.time()
    lst.sort()
    t1 = time.time()
    state['time'] = t1 - t0

if __name__ == '__main__':
    manager = multiprocessing.Manager()
    state = manager.dict(list_size=5*1000*1000)  # shared state
    p = multiprocessing.Process(target=run_test, args=(state,))
    p.start()
    p.join()
    print 'time to sort: %.3f' % state['time']
    print 'my PID is %d, sleeping for a minute...' % os.getpid()
    time.sleep(60)
    # at this point you can inspect the running process to see that it
    # does not consume excess memory

Бонусный ответ

Трудно дать ответ на вопрос о бонусе, так как вопрос неясен. "Концепция бесплатного списка" - это именно та концепция, стратегия реализации, которая должна быть явно закодирована поверх обычного распределителя Python. Большинство типов Python не используют эту стратегию распределения, например, она не используется для экземпляров классов, созданных с помощью оператора class. Внедрение бесплатного списка не сложно, но оно довольно продвинуто и редко проводится без уважительной причины. Если какой-либо автор расширений решил использовать бесплатный список для одного из его типов, можно ожидать, что они знают о компромиссе, который предлагает бесплатный список, - получают сверхбыстрое распределение/освобождение за счет некоторого дополнительного пространства (для объекты в свободном списке и сам свободный список) и невозможность повторного использования памяти для чего-то еще.