как узнать память работы кода питон
Понимаем, сколько памяти используют ваши объекты Python
Оказывается, нетривиально выяснить, сколько памяти фактически потребляется. В этой статье я расскажу о тонкостях управления памятью объекта Python и покажу, как точно измерить потребляемую память.
Также я запустил числа на 64-битном Python 2.7. В Python 3 числа иногда немного отличаются (особенно для строк, которые всегда являются Unicode), но концепции одинаковы.
Практическое исследование использования памяти Python
Во-первых, давайте немного разберемся и получим конкретное представление о фактическом использовании памяти объектами Python.
Встроенная функция sys.getsizeof()
Модуль sys стандартной библиотеки предоставляет функцию getsizeof(). Эта функция принимает объект (и необязательный параметр по умолчанию), вызывает метод sizeof() объекта и возвращает результат, поэтому вы также можете сделать ваши объекты инспектируемыми.
Измерение памяти объектов Python
Давайте начнем с некоторых числовых типов:
Интересно. Целое число занимает 24 байта.
Вот это да. 80 байтов! Это действительно заставляет задуматься о том, хотите ли вы представлять большое количество вещественных чисел как числа с плавающей запятой или десятичные дроби.
Давайте перейдем к строкам и коллекциям:
Хорошо. Пустая строка занимает 37 байтов, и каждый дополнительный символ добавляет еще один байт. Это многое говорит о компромиссе между сохранением нескольких коротких строк, когда вы будете платить 37 байтов за каждую, а не одну длинную строку, где вы платите только один раз.
Строки Unicode ведут себя аналогично, за исключением того, что служебные данные составляют 50 байтов, и каждый дополнительный символ добавляет 2 байта. Это стоит учитывать, если вы используете библиотеки, которые возвращают строки Unicode, но ваш текст может быть представлен в виде простых строк.
Кстати, в Python 3 строки всегда имеют Unicode, а служебные данные составляют 49 байт (они где-то сохранили байт). Объект байтов имеет служебную информацию только 33 байта. Если у вас есть программа, которая обрабатывает много коротких строк в памяти, и вы заботитесь о производительности, рассмотрите Python 3.
В чем дело? Пустой список занимает 72 байта, но каждый дополнительный int добавляет всего 8 байтов, где размер int составляет 24 байта. Список, который содержит длинную строку, занимает всего 80 байтов.
Ответ прост. Список не содержит сами объекты int. Он просто содержит 8-байтовый (в 64-битных версиях CPython) указатель на фактический объект int. Это означает, что функция getsizeof() не возвращает фактическую память списка и всех объектов, которые он содержит, а только память списка и указатели на свои объекты. В следующем разделе я представлю функцию deep_getsizeof(), которая решает эту проблему.
Наборы и словари якобы вообще не растут при добавлении элементов, но отмечают огромные накладные расходы.
Суть в том, что у объектов Python огромные фиксированные накладные расходы. Если ваша структура данных состоит из большого количества объектов коллекций, таких как строки, списки и словари, которые содержат небольшое количество элементов каждый, вы много платите.
Функция deep_getsizeof()
Теперь, когда я напугал вас до полусмерти и продемонстрировал, что sys.getsizeof() может только сказать вам, сколько памяти занимает примитивный объект, давайте посмотрим на более адекватное решение. Функция deep_getsizeof() рекурсивно выполняет детализацию и вычисляет фактическое использование памяти графом объектов Python.
У этой функции есть несколько интересных аспектов. Она учитывает объекты, на которые ссылаются несколько раз, и учитывает их только один раз, отслеживая идентификаторы объектов. Другая интересная особенность реализации заключается в том, что она в полной мере использует абстрактные базовые классы модуля коллекций. Это позволяет функции очень лаконично обрабатывать любую коллекцию, которая реализует базовые классы Mapping или Container, вместо непосредственного обращения к множеству типов коллекций, таких как: строка, Unicode, байты, список, кортеж, dict, frozendict, OrderedDict, set, frozenset и т.д.
Давайте посмотрим на это в действии:
Строка длиной 7 занимает 44 байта (37 служебных данных + 7 байтов для каждого символа).
Пустой список занимает 72 байта (только накладные расходы).
python deep_getsizeof ([x], set ()) 124
Список, содержащий строку x, занимает 124 байта (72 + 8 + 44).
Список, содержащий строку x 5 раз, занимает 156 байтов (72 + 5 * 8 + 44).
Последний пример показывает, что deep_getsizeof() подсчитывает ссылки на один и тот же объект (строку x) только один раз, но подсчитывается указатель каждой ссылки.
Баг или фича
Оказывается, что у CPython есть несколько хитростей, поэтому числа, которые вы получаете от deep_getsizeof(), не полностью отражают использование памяти программой Python.
Подсчет ссылок
Python управляет памятью, используя семантику подсчета ссылок. Когда на объект больше не ссылаются, его память освобождается. Но пока есть ссылка, объект не будет освобожден. Такие вещи, как циклические ссылки, могут вас сильно укусить.
Маленькие объекты
CPython управляет небольшими объектами (менее 256 байтов) в специальных пулах на 8-байтовых границах. Есть пулы для 1-8 байтов, 9-16 байтов и вплоть до 249-256 байтов. Когда объект размером 10 выделяется, он выделяется из 16-байтового пула для объектов размером 9-16 байт. Таким образом, хотя он содержит только 10 байтов данных, он будет стоить 16 байтов памяти. Если вы выделяете 1 000 000 объектов размером 10, вы фактически используете 16 000 000 байтов, а не 10 000 000 байтов, как вы можете предположить. Эти 60% накладных расходов явно не тривиальны.
Целые числа
CPython хранит глобальный список всех целых чисел в диапазоне [-5, 256]. Эта стратегия оптимизации имеет смысл, потому что маленькие целые числа всплывают повсюду, и, учитывая, что каждое целое число занимает 24 байта, оно экономит много памяти для типичной программы.
Это также означает, что CPython предварительно выделяет 266 * 24 = 6384 байта для всех этих целых чисел, даже если вы не используете большинство из них. Вы можете проверить это с помощью функции id(), которая дает указатель на фактический объект. Если вы называете id(x) несколько для любого x в диапазоне [-5, 256], вы будете каждый раз получать один и тот же результат (для одного и того же целого числа). Но если вы попробуете это для целых чисел за пределами этого диапазона, каждый из них будет отличаться (новый объект создается на лету каждый раз).
Вот несколько примеров в этом диапазоне:
Вот несколько примеров за пределами диапазона:
Память Python против системной памяти
CPython является своего рода притяжательным. Во многих случаях, когда на объекты памяти в вашей программе больше не ссылаются, они не возвращаются в систему (например, маленькие объекты). Это хорошо для вашей программы, если вы выделяете и освобождаете много объектов (которые принадлежат одному и тому же 8-байтовому пулу), потому что Python не должен беспокоить систему, что относительно дорого. Но это не так здорово, если ваша программа обычно использует X байтов и при некоторых временных условиях она использует в 100 раз больше (например, анализирует и обрабатывает большой файл конфигурации только при запуске).
Теперь эта память 100X может быть бесполезно захвачена в вашей программе, никогда больше не использоваться и лишать систему возможности выделять ее другим программам. Ирония заключается в том, что если вы используете модуль обработки для запуска нескольких экземпляров вашей программы, вы строго ограничите количество экземпляров, которые вы можете запустить на данном компьютере.
Профилировщик памяти
Чтобы измерить и измерить фактическое использование памяти вашей программой, вы можете использовать модуль memory_profiler. Я немного поиграл с этим, и я не уверен, что доверяю результатам. Он очень прост в использовании. Вы декорируете функцию (может быть главной (0 функция)) с помощью декоратора @profiler, и когда программа завершает работу, профилировщик памяти выводит на стандартный вывод удобный отчет, который показывает общее количество и изменения в памяти для каждой строки. Вот пример программы, которую я запускал под профилировщиком:
Заключение
CPython использует много памяти для своих объектов. Он использует различные приемы и оптимизации для управления памятью. Отслеживая использование памяти вашим объектом и зная модель управления памятью, вы можете значительно уменьшить объем памяти вашей программы.
🐍 Помнить всё. Как работает память в Python
Python многое делает за нас. Мы привыкли не заботиться об управлении памятью и о написании соответствующего кода. Пусть эти процессы и скрыты, но без их понимания трудно подготовить производительный код для высоконагруженных задач. В этой статье мы рассмотрим модель памяти Python и то, как интерпретатор Python взаимодействует с оперативной памятью компьютера.
Диспетчер памяти: «командовать парадом буду я»
Диспетчер памяти — своеобразный портье, который регистрирует и расселяет гостей отеля. Каждый постоялец получает ключ с номером комнаты, так что ни один из гостей не может заселиться не в свой номер. Две программы не могут одновременно записать переменную в одно место виртуальной памяти.
Фактически за это отвечает даже не диспетчер задач, который ожидает гостей за регистрационной стойкой, а GIL — глобальная блокировка интерпретатора. GIL гарантирует: в один и тот же момент времени байт-код выполняется только одним потоком. Главное преимущество — безопасная работа с памятью, а основной недостаток в том, что многопоточное выполнение программ Python требует специфических решений.
Очевидно, программа не сама выполняет сохранение и освобождение памяти — ведь мы не пишем соответствующих инструкций. Интерпретатор лишь запрашивает диспетчер памяти сделать это. А диспетчер уже делегирует работу, связанную с хранением данных, аллокаторам — распределителям памяти.
Организация доступной виртуальной памяти
Непосредственно с оперативной памятью взаимодействует распределитель сырой памяти (raw memory allocator). Поверх него работают аллокаторы, реализующие стратегии управления памятью, специфичные для отдельных типов объектов. Объекты разных типов — например, числа и строки — занимают разный объем, к ним применяются разные механизмы хранения и освобождения памяти. Аллокаторы стараются не занимать лишнюю память до тех пор, пока она не станет совершенно необходимой — этот момент определен стратегией распределения памяти CPython.
Python использует динамическую стратегию, то есть распределение памяти выполняется во время выполнения программы. Виртуальная память Python представляет иерархическую структуру, оптимизированную под объекты Python размером менее 256 Кб:
Блок содержит не более одного объекта Python и находится в одном из трех состояний:
Арена
Информацию о текущем распределении памяти в аренах, пулах и блоках можно посмотреть, запустив функцию sys._debugmallocstats() :
Чтобы не произошло утечки памяти, диспетчер памяти должен отследить, что вся выделенная память освободится после завершения работы программы. То есть при завершении программы CPython дает задание очистить все арены.
Освобождение памяти: счетчик ссылок, сборщик мусора
Для освобождения памяти используются два механизма: счетчик ссылок и сборщик мусора.
Однако счетчик ссылок неспособен отследить ситуации с циклическими ссылками. К примеру, возможна ситуация, когда два объекта ссылаются друг на друга, но оба уже не используются программой. Для борьбы с такими зависимостями используется сборщик мусора ( garbage collector ).
Заключение
Сохранение и освобождение блоков памяти требует времени и вычислительных ресурсов. Чем меньше блоков задействовано, тем выше скорость работы программы. Позволим себе дать несколько советов, касающихся экономной работы с памятью:
Как получить текущий процессор и оперативную память в Python?
каков ваш предпочтительный способ получения текущего состояния системы (текущий процессор, оперативная память, свободное место на диске и т. д.) в Python? Бонусные баллы для платформ * nix и Windows.
кажется, есть несколько возможных способов извлечь это из моего поиска:
использование библиотеки, такой как PSI (который в настоящее время, кажется, не активно развивается и не поддерживается на нескольких платформах) или что-то вроде pystatgrab (опять никакой активности с тех пор 2007 кажется и нет поддержки Windows).
использование специфичного для платформы кода, например, с помощью os.popen(«ps») или аналогичный для систем *nix и MEMORYSTATUS in ctypes.windll.kernel32 (см. этот рецепт на ActiveState) для платформы Windows. Можно было бы поместить класс Python вместе со всеми этими фрагментами кода.
это не то, что эти методы плохи, но есть ли уже хорошо поддерживаемый, многоплатформенный способ сделать то же самое?
11 ответов:
библиотека psutil даст вам некоторую системную информацию (использование процессора / памяти) на различных платформах:
psutil-это модуль, предоставляющий интерфейс для извлечения информации о запущенных процессах и использовании системы (CPU, память) портативным способом с помощью Python, реализующий многие функции, предлагаемые такими инструментами, как ps, top и Windows task manager.
в настоящее время он поддерживает Linux, Windows, OSX, Sun Solaris, FreeBSD, OpenBSD и NetBSD, как 32-разрядные, так и 64-разрядные архитектуры, с версиями Python от 2.6 до 3.5 (пользователи Python 2.4 и 2.5 могут использовать версию 2.1.3).
использовать библиотека psutil. Для меня на Ubuntu, pip установлен 0.4.3. Вы можете проверить свою версию psutil, сделав это в Python:
чтобы получить некоторые характеристики памяти и процессора:
мне тоже нравится это делать:
что дает текущее использование памяти вашего скрипта Python.
есть еще несколько подробных примеров на pypi страница для 4.3.0 и 0.5.0.
для Ubuntu 16 и 14, установка из pip дала мне версию 4.3.0, которая не имеет метода phymem_usage (). Чтобы получить 0.5.0, вы можете сделать pip install psutil==0.5.0 или скачать tar.файл GZ, то ли
ниже кодов, без внешних библиотек работал для меня. Я тестировал на Python 2.7.9
загрузка ЦП
и использование ОЗУ, всего, используется и бесплатно
один лайнер для использования ОЗУ только с зависимостью stdlib:
вот что я собрал некоторое время назад, это только windows, но может помочь вам получить часть того, что вам нужно сделать.
Примечание: интерфейс/процесс WMI также доступен для выполнения схожие задачи Я не использую его здесь, потому что текущий метод покрывает мои потребности, но если когда-нибудь это необходимо расширить или улучшить, то, возможно, потребуется исследовать инструменты WMI, доступные.
«. текущее состояние системы (текущий процессор, оперативная память, свободное место на диске и т. д.) «И» *Nix и платформы Windows » могут быть трудной комбинацией для достижения.
операционные системы принципиально отличаются тем, как они управляют этими ресурсами. Действительно, они различаются по основным понятиям, таким как определение того, что считается системой и что считается временем приложения.
«свободное место на диске»? Что считается » дисковое пространство?»Все разделы всех устройств? А как же иностранцы разделы в среде с несколькими загрузками?
Я не думаю, что есть достаточно ясный консенсус между Windows и *nix, что делает это возможным. Действительно, может даже не быть никакого консенсуса между различными операционными системами, называемыми Windows. Есть ли один API Windows, который работает как для XP, так и для Vista?
Я чувствую, что эти ответы были написаны для Python 2, и в любом случае никто не упоминал о стандарте resource пакет, который доступен для Python 3. Он предоставляет команды для получения ресурса ограничения данного процесса (вызывающий процесс Python по умолчанию). Это не то же самое, что получить текущий использование ресурсов системой в целом, но она может решить некоторые из тех же проблем, например: «я хочу убедиться, что я только используйте X много ОЗУ с этим скриптом.»
вы можете использовать psutil или psmem с подпроцессом пример кода
мы решили использовать обычный источник информации для этого, потому что мы могли бы найти мгновенные колебания в свободной памяти и чувствовал запрос meminfo источник данных был полезным. Это также помогло нам получить несколько связанных параметров, которые были предварительно разобраны.
вывод Для справки (мы убрали все новые строки для дальнейшего анализа)
MemTotal: 1014500 kB MemFree: 562680 kB MemAvailable: 646364 КБ Буферы: 15144 КБ кэшированный: 210720 КБ SwapCached: 0 КБ активный: 261476 КБ Неактивный: 128888 КБ активный (Анон): 167092 КБ неактивный (Анон): 20888 КБ Активный(файл): 94384 КБ не активен(файл): 108000 КБ удаления и недоступные для удаления: 3652 КБ Mlocked: 3652 kB SwapTotal: 0 kB SwapFree: 0 kB Dirty: 0 kB обратная запись: 0 kB AnonPages: 168160 kB отображено: 81352 kB Shmem: 21060 kB плита: 34492 kB SReclaimable: 18044 kB SUnreclaim: 16448 kB KernelStack: 2672 kB PageTables: 8180 kB NFS_Unstable: 0 kB Отказов: 0 КБ WritebackTmp: 0 КБ Committlimit: 507248 kB Committed_AS: 1038756 kB VmallocTotal: 34359738367 КБ VmallocUsed: 0 КБ VmallocChunk: 0 КБ HardwareCorrupted: 0 kB AnonHugePages: 88064 kB CmaTotal: 0 kB CmaFree: 0 kB HugePages_Total: 0 HugePages_Free: 0 HugePages_Rsvd: 0 HugePages_Surp: 0 Hugepagesize: 2048 kB DirectMap4k: 43008 kB DirectMap2M: 1005568 kB
этот скрипт для использования ЦП:
Управление памятью в Python
Одна из главных проблем при написании крупных (относительно) программ на Python — минимизация потребления памяти. Однако управлять памятью здесь легко — если вас вообще это волнует. Память в Python выделяется прозрачно, управление объектами происходит с помощью системы счётчиков ссылок (reference count), и память высвобождается, когда счётчик падает до нуля. В теории всё прекрасно. А на практике вам нужно знать несколько вещей об управлении памятью в Python, чтобы ваши программы эффективно её использовали. Первая вещь, надо хорошо в ней разбираться: размеры основных объектов в Python. И вторая вещь: как устроено управление «под капотом» языка.
Начнём с размеров объектов. В Python есть много примитивных типов данных: целые числа (int), long (версия int с неограниченной точностью), числа с плавающей запятой (они же числа с двойной точностью, double), кортежи (tuple), строковые значения, списки, словари и классы.
Основные объекты
Давайте напишем функцию, показывающую размер объектов (рекурсивно, если нужно):
Теперь с помощью этой функции можно исследовать размеры основных типов данных:
Если у вас 32-битный Python 2.7x, то вы увидите:
А если 64-битный Python 2.7x, то увидите:
Давайте сосредоточимся на 64-битной версии (в основном потому, что в нашем случае она более востребована). None занимает 16 байтов. int — 24 байта, в три раза больше по сравнению с int64_t в языке С, хотя это в какой-то мере machine-friendly целое число. Минимальный размер значений типа long (с неограниченной точностью), используемых для представления чисел больше 2 63 – 1, это — 36 байтов. Затем они увеличиваются линейно, как логарифм представляемого числа.
Числа с плавающей запятой в Python зависят от реализации, но похожи на числа с двойной точностью в C. Однако они не занимают всего лишь 8 байтов:
На 32-битной платформе выдаёт:
Это опять втрое больше, чем предположил бы программист на C. А что насчёт строковых значений?
На 32-битной платформе:
Пустое строковое значение занимает 37 байтов в 64-битной среде! Затем потребление памяти увеличивается в соответствии с размером (полезного) значения.
Давайте разберёмся и с другими часто востребованными структурами: кортежами, списками и словарями. Списки (реализованные как списки массивов, а не как связные списки, со всеми вытекающими) — это массивы ссылок на Python-объекты, что позволяет им быть гетерогенными. Их размеры:
Пустой список занимает 72 байта. Размер пустого std::list() в 64-битном С — всего 16 байтов, в 4—5 раз меньше. Что насчёт кортежей? И словарей?
На 32-битной платформе выдаёт:
Последний пример особенно интересен, потому что он «не складывается». Пары ключ/значение занимают 72 байта (их компоненты занимают 38 + 24 = 62 байта, а ещё 10 тратится на саму пару), но весь словарь весит уже 280 байтов (а не минимально необходимые 144 = 72 × 2 байта). Словарь считается эффективной структурой данных для поиска, и две вероятные реализации будут занимать памяти больше, чем необходимый минимум. Если это какое-то дерево, то приходится расплачиваться за внутренние ноды, содержащие ключ и два указателя на дочерние ноды. Если это хеш-таблица, то ради хорошей производительности нужно иметь место для свободных записей.
Эквивалентная (относительно) структура std::map из C++ при создании занимает 48 байтов (пока ещё пустая). А пустое строковое значение в C++ требует 8 байтов (затем размер линейно растёт вместе с размером строки). Целочисленное значение — 4 байта (32 бит).
И что нам всё это даёт? Тот факт, что пустое строковое значение занимает 8 или 37 байтов, мало что меняет. Действительно. Но лишь до тех пор, пока ваш проект не начнёт разрастаться. Тогда вам придётся очень аккуратно следить за количеством создаваемых объектов, чтобы ограничить объём потребляемой приложением памяти. Для настоящих приложений это проблема. Чтобы разработать действительно хорошую стратегию управления памятью, нам нужно следить не только за размером новых объектов, но и за количеством и порядком их создания. Для Python-программ это очень важно. Давайте теперь разберёмся со следующим ключевым моментом: с внутренней организацией выделения памяти в Python.
Внутреннее управление памятью
Чтобы ускорить выделение памяти (и её повторное применение), Python использует ряд списков для маленьких объектов. Каждый список содержит объекты одного размера: может быть один список для объектов от 1 до 8 байтов, другой — для объектов 9—16 байтов и т. д. Когда нужно создать маленький объект, мы вновь используем свободный блок в списке или выделяем новый.
Есть несколько нюансов, как Python распределяет эти списки по блокам, пулам и «аренам»: несколько блоков формируют пул, пулы собираются в арену и т. д. Но мы в это углубляться не будем (если хотите, то можете почитать мысли Эвана Джонса о том, как улучшить выделение памяти в Python). Нам важно знать, что эти списки неуменьшаемы.
В самом деле: если элемент (размером x) удалён из памяти (стёрта ссылка на него), то занимавшийся им объём не возвращается в пул глобальной памяти Python (в том числе и в систему), а помечается свободным и добавляется к списку свободных элементов размером x. Занимаемый мёртвым объектом объём может быть использован вновь, если понадобится другой объект подходящего размера. А если подходящего мёртвого объекта нет, то создаётся новый.
Если память с маленькими объектами никогда не освобождается, то мы приходим к неизбежному выводу, что эти списки с маленькими объектами могут только расти, они никогда не уменьшаются, а значит, в каждый момент времени в памяти вашего приложения преобладают размещённые в ней многочисленные маленькие объекты.
Следовательно, старайтесь размещать в памяти только то количество маленьких объектов, которое нужно для какой-то одной задачи, отдавая предпочтение циклам, в которых создаётся/обрабатывается небольшое количество элементов, а не паттернам, где списки сначала создаются с помощью синтаксиса генерирования, а потом обрабатываются.
Хотя второй вариант более соответствует духу Python, он менее удачен: в конце концов появится большое количество маленьких объектов, которые заполнят соответствующие списки, и даже если какой-то список станет мёртвым, то объекты в нём (теперь уже все находящиеся в списке свободных объектов) всё ещё будут занимать много памяти.
Увеличить списки свободных элементов — не особая проблема, потому что эта память всё ещё доступна для Python-программы. Но с точки зрения ОС размер вашей программы равен общему размеру выделенной для Python памяти. И только под Windows память возвращается в кучу ОС (и применяется для размещения и других объектов, помимо маленьких), а под Linux общий объём используемой вашим приложением памяти будет только расти.
На 64-битном компьютере она выводит:
Программа создаёт n = 1 000 000 целых чисел (n × 24 байта =
23 Мб) и дополнительный список ссылок (n × 8 байтов =
7,6 Мб), и в сумме получаем
31 Мб. copy.deepcopy копирует оба списка, и копии занимают
В этом примере в сумме занято
73 Мб, что более чем вдвое превышает объём, необходимый для хранения списка, весящего
31 Мб. Как видите, при потере бдительности порой возникают очень неприятные сюрпризы с точки зрения потребления памяти!
Вы можете получить иные результаты на других платформах и других версиях Python.
Pickle
Pickle — стандартный способ (де)сериализации Python-объектов в файл. Каково его потребление памяти? Он создаёт дополнительные копии данных или работает умнее? Рассмотрим короткий пример:
При первом вызове мы профилируем создание pickled-данных, а при втором вызове заново считываем их (можно закомментировать функцию, чтобы она не вызывалась). При использовании memory_profiler в ходе создания данных потребляется много памяти:
А при считывании — немного меньше:
Так что picklе очень плохо влияет на потребление памяти. Исходный список занимает около 230 Мб, а при сериализации потребляется ещё примерно столько же.
C другой стороны, десериализация выглядит более эффективной. Потребляется больше памяти, чем исходный список (300 Мб вместо 230), но это хотя бы не вдвое больше.
В целом лучше избегать (де)сериализации в приложениях, чувствительных к потреблению памяти. Какие есть альтернативы? Сериализация сохраняет всю структуру данных, так что позднее вы сможете полностью восстановить её из получившегося файла. Но это не всегда нужно. Если файл содержит список, как в предыдущем примере, то, возможно, целесообразно использовать простой, текстовый формат. Давайте посмотрим, что это даёт.
Простейшая (naïve) реализация:
При записи потребляется гораздо меньше памяти. Всё ещё создаётся много временных маленьких объектов (примерно 60 Мб), но это не сравнить с удвоенным потреблением. Чтение сравнимо по затратам (используется чуть меньше памяти).
Или, ещё лучше, применяйте массивы Numpy (или PyTables). Но это уже совсем другая история. В то же время в директории Theano/doc/tutorial вы можете почитать другое руководство по загрузке и сохранению.
Цели архитектуры Python никак не совпадают, допустим, с целями архитектуры C. Последний спроектирован так, чтобы дать вам хороший контроль над тем, что вы делаете, за счёт более сложного и явного программирования. А первый спроектирован так, чтобы вы могли писать код быстрее, но при этом язык прячет большинство подробностей реализации (если не все). Хотя это звучит красиво, но игнорирование неэффективных реализаций языка в production-среде порой приводит к неприятным последствиям, иногда неисправимым. Надеюсь, что знание этих особенностей Python при работе с памятью (архитектурных особенностей!) поможет вам писать код, который будет лучше соответствовать требованиям production, хорошо масштабироваться или, напротив, окажется горящим адом для памяти.