[Перевод] Если данные не помещаются в память. Простейшие методы

6d9123a92cd8eaa7935353ab9ed3f029.jpg


Самка трубкозуба с детёнышем. Фото: Scotto Bear, CC BY-SA 2.0

Вы пишете программу для обработки данных, она отлично проходит тест на небольшом файле, но падает на реальной нагрузке.

Проблема в нехватке памяти. Если у вас 16 гигабайт ОЗУ, вы не сможете туда загрузить стогигабайтный файл. В какой-то момент у ОС закончится память, она не сможет выделить новую, и программа вылетит.

Что делать?
Ну, можете развернуть кластер Big Data, всего-то:

  • Найти кластер компьютеров.
  • За неделю его настроить.
  • Изучить новый API и переписать свой код.


Это дорого и неприятно. К счастью, зачастую и не нужно.

Нам требуется простое и лёгкое решение: обрабатывать данные на одном компьютере, с минимальной настройкой и максимальным использованием уже подключенных библиотек. Почти всегда это возможно с помощью простейших методов, которые иногда называют «вычислениями вне памяти» (out-of-core computation).

В этой статье обсудим:

  • Зачем нам вообще нужна оперативная память.
  • Самый простой способ обработать данные, которые не помещаются в память — потратить немножко денег.
  • Три основных программных метода обработки чрезмерных объёмов данных: сжатие, разбиение на блоки и индексирование.


В будущих статьях на практике покажем, как применять эти методы с конкретными библиотеками, таким как NumPy и Pandas. Но сначала теория.

Зачем вообще нужна оперативная память?


Прежде чем перейти к обсуждению решений, давайте проясним, почему эта проблема вообще существует. В оперативную память (RAM) можно записывать данные, но и на жёсткий диск тоже, так зачем вообще нужна RAM? Диск дешевле, у него обычно нет проблем с нехваткой места, почему же просто не ограничиться чтением и записью с диска?

Теоретически это может сработать. Но даже современные быстрые SSD работают намного, намного медленнее, чем RAM:

  • Чтение с SSD: ~16 000 наносекунд
  • Чтение из RAM: ~100 наносекунд


Для быстрых вычислений у нас не остаётся выбора: данные приходится записывать в ОЗУ, иначе код замедлится в 150 раз.

Самое простое решение: больше оперативной памяти


Самое простое решение проблемы нехватки оперативной памяти — потратить немного денег. Вы можете купить мощный компьютер, сервер или арендовать виртуальную машину с большим количеством памяти. В ноябре 2019 года быстрый поиск и очень краткое сравнение цен даёт такие варианты:

  • Купить Thinkpad M720 Tower с 6 ядрами и 64 ГБ оперативной памяти за $1074
  • Арендовать в облаке виртуальную машину с 64 ядрами и 432 ГБ оперативной памяти за $3,62/час


Это просто цифры после быстрого поиска. Проведя хорошее исследование, вы наверняка найдёте более выгодные предложения.

Потратить немного денег на аппаратное обеспечение, чтобы данные поместились в ОЗУ, — зачастую самое дешёвое решение. В конце концов, наше время дорого. Но иногда этого недостаточно.

Например, если вы выполняете много заданий по обработке данных в течение определённого периода времени, облачные вычисления могут быть естественным решением, но также и дорогостоящим. На одном из наших проектов такие затраты на вычисления израсходовали бы весь прогнозируемый доход от продукта, включая самый важный доход, необходимый для выплаты моей зарплаты.

Если покупка/аренда большого объёма RAM не решает проблему или невозможна, следующий шаг — оптимизировать само приложение, чтобы оно расходовало меньше памяти.

Техника № 1. Сжатие


Сжатие позволяет поместить те же данные в меньший объём памяти. Есть две формы сжатия:

  • Без потерь: после сжатия сохраняется в точности та же информация, что и в исходных данных.
  • С потерями: сохраняемые данные теряют некоторые детали, но в идеале это не сильно влияет на результаты расчёта.


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

Что нам нужно, так это сжатие представления данных в памяти.

Предположим, в ваших данных хранится только два возможных значения, и больше ничего: "AVAILABLE" и "UNAVAILABLE". Вместо сохранения строк с 10 байтами или более на запись, вы можете сохранить их как логические значения True или False, которые кодируются просто одни байтом. Можете сжать информацию даже до одного бита, уменьшив расход памяти ещё в восемь раз.

Техника № 2. Разбиение на блоки, загрузка данных по одному блоку за раз


Фрагментация полезна в ситуации, когда данные не обязательно загружать в память одновременно. Вместо этого мы можем загружать их частями, обрабатывая по одному фрагменту за раз (или, как обсудим в следующей статье, несколько частей параллельно).

Предположим, вы хотите найти самое большое слово в книге. Можете загрузить в память сразу все данные:

largest_word = ""
for word in book.get_text().split():
    if len(word) > len(largest_word):
        largest_word = word


Но если книга не помещается в память, можно загрузить её постранично:

largest_word = ""
for page in book.iterpages():
    for word in page.get_text().split():
        if len(word) > len(largest_word):
            largest_word = word


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

Техника № 3. Индексация, когда требуется только подмножество данных


Индексирование полезно, если нужно использовать только подмножество данных и вы собираетесь загружать разные подмножества в разное время.

В принципе, в такой ситуации можно отфильтровать нужную часть и отбросить ненужное. Но фильтрация работает медленно и не оптимально, потому что придётся сначала загрузить в память много лишних данных, прежде чем их отбросить.

Если вам нужна только часть данных, вместо фрагментации лучше использовать индекс — выжимку данных, которая указывает на их реальное местоположение.

Представьте, что вы хотите прочитать только фрагменты книги, где упоминается трубкозуб (симпатичное млекопитающее на фотографии в начале статьи). Если проверять все страницы по очереди, то в память будет загружена по частям вся книга, страница за страницей, в поисках трубкозубов — и это займёт довольно много времени.

Или можете сразу открыть алфавитный индекс в конце книги — и найти слово «трубкозуб». Там указано, что упоминания слова есть на страницах 7, 19 и 120–123. Теперь можно прочитать эти страницы, и только их, что намного быстрее.

Это эффективный метод, потому что индекс намного меньше, чем вся книга, так что намного проще загрузить в память только индекс для поиска соответствующих данных.

Самый простой метод индексирования


Самый простой и распространённый способ индексирования — именование файлов в каталоге:

mydata/
    2019-Jan.csv
    2019-Feb.csv
    2019-Mar.csv
    2019-Apr.csv
    ...


Если вам нужны данные за март 2019 года, вы просто загружаете файл 2019-Mar.csv — нет необходимости загружать данные за февраль, июль или любой другой месяц.

Дальше: применение этих методов


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

Те же методы используются в различных программных пакетах и инструментах. На них построены даже высокопроизводительные системы Big Data: например, параллельная обработка отдельных фрагментов данных.

В следующих статьях рассмотрим, как применять эти методы в конкретных библиотеках и инструментах, в том числе NumPy и Pandas.

© Habrahabr.ru