Как написать программу на Python с эффективным использованием памяти? - PullRequest
9 голосов
/ 02 ноября 2009

Говорят, что Python автоматически управляет памятью. Я запутался, потому что у меня программа на Python постоянно использует более 2 ГБ памяти.

Это простой многопоточный загрузчик и распаковщик двоичных данных.

def GetData(url):
    req = urllib2.Request(url)
    response = urllib2.urlopen(req)
    data = response.read() // data size is about 15MB
    response.close()
    count = struct.unpack("!I", data[:4])
    for i in range(0, count):
        UNPACK FIXED LENGTH OF BINARY DATA HERE
        yield (field1, field2, field3)

class MyThread(threading.Thread):
    def __init__(self, total, daterange, tickers):
        threading.Thread.__init__(self)

    def stop(self):
        self._Thread__stop()

    def run(self):
        GET URL FOR EACH REQUEST
        data = []
        items = GetData(url)
        for item in items:
            data.append(';'.join(item))
        f = open(filename, 'w')
        f.write(os.linesep.join(data))
        f.close()

Работает 15 потоков. Каждый запрос получает 15 МБ данных, распаковывает их и сохраняет в локальном текстовом файле. Как эта программа может использовать более 2 ГБ памяти? Нужно ли выполнять какие-либо работы по переработке памяти в этом случае? Как узнать, сколько памяти используют каждый объект или функция?

Буду признателен за советы и рекомендации о том, как сохранить программу на Python в режиме с эффективным использованием памяти.

Редактировать: Вот вывод "cat / proc / meminfo"

MemTotal:        7975216 kB
MemFree:          732368 kB
Buffers:           38032 kB
Cached:          4365664 kB
SwapCached:        14016 kB
Active:          2182264 kB
Inactive:        4836612 kB

Ответы [ 8 ]

11 голосов
/ 02 ноября 2009

Как уже говорили другие, вам нужно как минимум следующие два изменения:

  1. Не создавайте огромный список целых чисел с range

    # use xrange
    for i in xrange(0, count):
        # UNPACK FIXED LENGTH OF BINARY DATA HERE
        yield (field1, field2, field3)
    
  2. не создавать огромную строку как полное тело файла для одновременной записи

    # use writelines
    f = open(filename, 'w')
    f.writelines((datum + os.linesep) for datum in data)
    f.close()
    

Еще лучше, вы можете написать файл как:

    items = GetData(url)
    f = open(filename, 'w')
    for item in items:
        f.write(';'.join(item) + os.linesep)
    f.close()
8 голосов
/ 02 ноября 2009

Основной виновник здесь, как упомянуто выше, вызов range (). Он создаст список с 15 миллионами участников, и он будет поглощать 200 МБ вашей памяти, а с 15 процессами - 3 ГБ.

Но также не читайте весь файл размером 15 МБ в data (), читайте по битам из ответа. Вставка этих 15 МБ в переменную будет занимать 15 МБ памяти больше, чем считывание по битам из ответа.

Возможно, вы захотите просто извлечь данные до тех пор, пока не закончится индата, и сравнить количество извлеченных данных с тем, что должно быть в первых байтах. Тогда вам не нужно ни range (), ни xrange (). Мне кажется более питоническим. :)

6 голосов
/ 02 ноября 2009

Рассмотрите возможность использования xrange () вместо range (), я считаю, что xrange является генератором, тогда как range () расширяет весь список.

Я бы сказал, что не читайте весь файл в память или не храните всю распакованную структуру в памяти.

В настоящее время вы храните оба в памяти, в то же время, это будет довольно большим. Таким образом, у вас есть как минимум две копии ваших данных в памяти плюс несколько метаданных.

Также финальная строка

    f.write(os.linesep.join(data))

Может означать, что у вас временно есть третья копия в памяти (большая строка со всем выходным файлом).

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

Использование генератора для разбора - неплохая идея. Подумайте о том, чтобы записать каждую запись после того, как вы сгенерировали ее (затем ее можно удалить и использовать повторно), или, если это вызывает слишком много запросов на запись, группируйте их, скажем, в 100 строк одновременно.

Аналогично, чтение ответа может быть выполнено кусками. Поскольку они фиксированные записи, это должно быть достаточно просто.

5 голосов
/ 02 ноября 2009

Последняя строка обязательно должна быть f.close()? Эти последние параны очень важны.

2 голосов
/ 02 ноября 2009

Есть 2 очевидных места, где вы храните большие объекты данных в памяти (переменная data в GetData() и data в MyThread.run() - эти два будут занимать около 500 МБ), и, вероятно, есть и другие места в пропущенном код. Есть как легко сделать память эффективной. Используйте response.read(4) вместо одновременного чтения всего ответа и сделайте то же самое в коде UNPACK FIXED LENGTH OF BINARY DATA HERE. Измените data.append(...) в MyThread.run() на

if not first:
    f.write(os.linesep)
f.write(';'.join(item))

Эти изменения сэкономят вам много памяти.

2 голосов
/ 02 ноября 2009

Вы могли бы выполнять больше работы в скомпилированном коде C, если преобразовали это в понимание списка:

data = []
items = GetData(url)
for item in items:
    data.append(';'.join(item))

до:

data = [';'.join(items) for items in GetData(url)]

Это на самом деле немного отличается от вашего исходного кода. В вашей версии GetData возвращает 3-кортеж, который возвращается в элементах. Затем вы перебираете этот триплет и добавляете ';'. Join (item) для каждого элемента в нем. Это означает, что вы получаете 3 записи, добавленные к данным для каждого триплета, считанного из GetData, каждая из которых ';'. Join'ed. Если элементы являются просто строками, то «;». Join вернет вам строку с каждым другим символом «;» - то есть ';'. join ("ABC") вернет "A; B; C". Я думаю, что вы на самом деле хотели , чтобы каждый триплет был сохранен обратно в список данных в виде трех значений триплета, разделенных точками с запятой. Это то, что генерирует моя версия.

Это также может несколько помочь с исходной проблемой памяти, поскольку вы больше не создаете столько значений Python. Помните, что переменная в Python имеет намного больше накладных расходов, чем у такого языка, как C. Поскольку каждое значение само является объектом, и добавьте издержки каждой ссылки на имя на этот объект, вы можете легко расширить теоретический требования к хранению в несколько раз. В вашем случае чтение 15Mb X 15 = 225Mb + накладные расходы каждого элемента каждой тройки, сохраняемого в виде строковой записи в вашем списке данных, могут быстро вырасти до вашего наблюдаемого размера 2Gb. Как минимум, моя версия вашего списка данных будет содержать только 1/3 записей в нем, плюс ссылки на отдельные элементы пропускаются, плюс итерация выполняется в скомпилированном коде.

2 голосов
/ 02 ноября 2009

Вы можете повысить эффективность использования этой программы, не считывая все 15 МБ из TCP-соединения, а вместо этого обрабатывая каждую строку во время чтения. Конечно, это заставит вас ждать удаленные серверы, но это нормально.

Python просто не очень эффективно использует память. Он не был построен для этого.

1 голос
/ 02 ноября 2009

Убедитесь, что вы удалили темы после их остановки. (используя del)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...