Как написать программу на Python с эффективным использованием памяти?
Говорят, что 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 ответов
Как уже говорили другие, вам нужно как минимум следующие два изменения:
Не создавайте огромный список целых чисел с
range
# use xrange for i in xrange(0, count): # UNPACK FIXED LENGTH OF BINARY DATA HERE yield (field1, field2, field3)
не создавайте огромную строку, так как полное тело файла будет записано сразу
# 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()
Основной виновник здесь, как упоминалось выше, вызов range(). Он создаст список с 15 миллионами участников, и это будет поглощать 200 МБ вашей памяти, а с 15 процессами это 3 ГБ.
Но также не читайте весь файл размером 15 МБ в data(), читайте по битам из ответа. Вставка этих 15 МБ в переменную будет занимать 15 МБ памяти больше, чем считывание бит за ответом.
Возможно, вы захотите просто извлечь данные до тех пор, пока вы не исчерпаете их, и сравнить количество извлеченных данных с тем, что должны быть в первых байтах. Тогда вам не нужно ни range(), ни xrange(). Мне кажется более питоническим.:)
Подумайте об использовании xrange() вместо range(), я считаю, что xrange является генератором, тогда как range () расширяет весь список.
Я бы сказал, что не читайте весь файл в память или не храните всю распакованную структуру в памяти.
В настоящее время вы сохраняете оба в памяти, в то же время, это будет довольно большим. Таким образом, у вас есть по крайней мере две копии ваших данных в памяти плюс несколько метаданных.
Также последняя строка
f.write(os.linesep.join(data))
На самом деле это может означать, что у вас временно есть третья копия в памяти (большая строка со всем выходным файлом).
Поэтому я бы сказал, что вы делаете это довольно неэффективно, сохраняя весь входной файл, весь выходной файл и достаточное количество промежуточных данных в памяти одновременно.
Использование генератора для разбора это довольно хорошая идея. Подумайте о том, чтобы записать каждую запись после того, как вы сгенерировали ее (затем ее можно удалить и использовать повторно), или, если это вызывает слишком много запросов на запись, сгруппируйте их, скажем, в 100 строк одновременно.
Кроме того, чтение ответа может быть сделано в кусках. Поскольку они являются фиксированными записями, это должно быть достаточно просто.
Последняя строка обязательно должна быть f.close()
? Эти последние параны очень важны.
Вы можете сделать больше своей работы в скомпилированном 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 очевидных места, где вы храните большие объекты данных в памяти (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))
Эти изменения сэкономят вам много памяти.
Вы можете повысить эффективность использования этой программы, не считывая все 15 МБ из TCP-соединения, а вместо этого обрабатывая каждую строку во время чтения. Конечно, это заставит вас ждать удаленные серверы, но это нормально.
Python просто не очень эффективно использует память. Это не было построено для этого.
Убедитесь, что вы удалили темы после их остановки. (с помощью del
)