Ответ 1
необходимое условие
-
В Python (далее я использую 64-битную сборку Python 3.6.5) все является объектом. Это имеет свои накладные расходы, и с помощью
getsizeof
мы можем видеть точно размер объекта в байтах:>>> import sys >>> sys.getsizeof(42) 28 >>> sys.getsizeof('T') 50
- Когда для создания дочернего процесса используется системный вызов fork (по умолчанию * nix, см.
multiprocessing.get_start_method()
), родительская физическая память не копируется и используется метод копирования -o n-write. - Дочерний процесс Fork по-прежнему будет сообщать полный RSS (размер резидентного набора) родительского процесса. В связи с этим PSS (пропорциональный размер набора) является более подходящим показателем для оценки использования памяти приложения разветвления. Вот пример со страницы:
- Процесс А имеет 50 КиБ неразделенной памяти
- Процесс B имеет 300 КБ неразделенной памяти
- И процесс A, и процесс B имеют 100 КиБ одной и той же области общей памяти
Поскольку PSS определяется как сумма неразделенной памяти процесса и доли памяти, используемой совместно с другими процессами, PSS для этих двух процессов выглядит следующим образом:
- PSS процесса A = 50 КиБ + (100 КиБ /2) = 100 КиБ
- PSS процесса B = 300 КиБ + (100 КиБ /2) = 350 КиБ
Фрейм данных
Не давайте смотреть на ваш DataFrame
одиночку. memory_profiler
поможет нам.
justpd.py
#!/usr/bin/env python3
import pandas as pd
from memory_profiler import profile
@profile
def main():
with open('genome_matrix_header.txt') as header:
header = header.read().rstrip('\n').split('\t')
gen_matrix_df = pd.read_csv(
'genome_matrix_final-chr1234-1mb.txt', sep='\t', names=header)
gen_matrix_df.info()
gen_matrix_df.info(memory_usage='deep')
if __name__ == '__main__':
main()
Теперь давайте использовать профилировщик:
mprof run justpd.py
mprof plot
Мы можем увидеть сюжет:
и построчная -l In trace:
Line # Mem usage Increment Line Contents
================================================
6 54.3 MiB 54.3 MiB @profile
7 def main():
8 54.3 MiB 0.0 MiB with open('genome_matrix_header.txt') as header:
9 54.3 MiB 0.0 MiB header = header.read().rstrip('\n').split('\t')
10
11 2072.0 MiB 2017.7 MiB gen_matrix_df = pd.read_csv('genome_matrix_final-chr1234-1mb.txt', sep='\t', names=header)
12
13 2072.0 MiB 0.0 MiB gen_matrix_df.info()
14 2072.0 MiB 0.0 MiB gen_matrix_df.info(memory_usage='deep')
Мы можем видеть, что кадр данных занимает ~ 2 ГиБ с пиком в ~ 3 ГиБ во время его построения. Что более интересно, так это вывод info
.
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4000000 entries, 0 to 3999999
Data columns (total 34 columns):
...
dtypes: int64(2), object(32)
memory usage: 1.0+ GB
Но info(memory_usage='deep')
("deep" означает глубокий самоанализ данных путем опроса object
dtype
s, см. Ниже) дает:
memory usage: 7.9 GB
Да?! Посмотрев за пределы процесса, мы можем убедиться, что цифры memory_profiler
верны. sys.getsizeof
также показывает то же значение для фрейма (наиболее вероятно из-за пользовательского __sizeof__
), как и другие инструменты, которые используют его для оценки выделенного gc.get_objects()
, например, pympler
.
# added after read_csv
from pympler import tracker
tr = tracker.SummaryTracker()
tr.print_diff()
дает:
types | # objects | total size
================================================== | =========== | ============
<class 'pandas.core.series.Series | 34 | 7.93 GB
<class 'list | 7839 | 732.38 KB
<class 'str | 7741 | 550.10 KB
<class 'int | 1810 | 49.66 KB
<class 'dict | 38 | 7.43 KB
<class 'pandas.core.internals.SingleBlockManager | 34 | 3.98 KB
<class 'numpy.ndarray | 34 | 3.19 KB
Так откуда берутся эти 7,93 ГиБ? Давайте попробуем объяснить это. У нас есть 4M строк и 34 столбца, что дает нам 134M значений. Это либо int64
либо object
(который является 64-битным указателем; подробное объяснение см. В разделе " Использование панд с большими данными"). Таким образом, у нас 134 * 10 ** 6 * 8/2 ** 20
~ 1022 МБ только для значений в кадре данных. Как насчет оставшихся ~ 6,93 ГиБ?
Струнная интернирование
Чтобы понять поведение, необходимо знать, что Python выполняет интернирование строк. Есть две хорошие статьи (одна, две) об интернировании строк в Python 2. Помимо изменения Unicode в Python 3 и PEP 393 в Python 3.3, C-структуры изменились, но идея та же. По сути, каждая короткая строка, которая выглядит как идентификатор, будет кэшироваться Python во внутреннем словаре, а ссылки будут указывать на одни и те же объекты Python. Другими словами, мы можем сказать, что он ведет себя как одиночка. Статьи, которые я упоминал выше, объясняют, какие значительные улучшения в профиле памяти и производительности он дает. Мы можем проверить, если строка интернированы с использованием interned
поле PyASCIIObject
:
import ctypes
class PyASCIIObject(ctypes.Structure):
_fields_ = [
('ob_refcnt', ctypes.c_size_t),
('ob_type', ctypes.py_object),
('length', ctypes.c_ssize_t),
('hash', ctypes.c_int64),
('state', ctypes.c_int32),
('wstr', ctypes.c_wchar_p)
]
Затем:
>>> a = 'name'
>>> b = '[email protected]#$'
>>> a_struct = PyASCIIObject.from_address(id(a))
>>> a_struct.state & 0b11
1
>>> b_struct = PyASCIIObject.from_address(id(b))
>>> b_struct.state & 0b11
0
С помощью двух строк мы также можем сравнивать идентификаторы (в случае CPython это делается при сравнении памяти).
>>> a = 'foo'
>>> b = 'foo'
>>> a is b
True
>> gen_matrix_df.REF[0] is gen_matrix_df.REF[6]
True
Из-за этого факта в отношении object
dtype
фрейм данных выделяет не более 20 строк (по одной на аминокислоты). Однако стоит отметить, что Pandas рекомендует категориальные типы для перечислений.
Память панд
Таким образом, мы можем объяснить наивную оценку в 7,93 ГиБ как:
>>> rows = 4 * 10 ** 6
>>> int_cols = 2
>>> str_cols = 32
>>> int_size = 8
>>> str_size = 58
>>> ptr_size = 8
>>> (int_cols * int_size + str_cols * (str_size + ptr_size)) * rows / 2 ** 30
7.927417755126953
Обратите внимание, что str_size
составляет 58 байт, а не 50, как мы видели выше для 1-символьного литерала. Это потому, что PEP 393 определяет компактные и некомпактные строки. Вы можете проверить это с помощью sys.getsizeof(gen_matrix_df.REF[0])
.
Фактическое потребление памяти должно составлять ~ 1 ГиБ, как сообщает gen_matrix_df.info()
, это в два раза больше. Можно предположить, что это как-то связано с (предварительным) распределением памяти, выполняемым Pandas или NumPy. Следующий эксперимент показывает, что это не без причины (несколько прогонов показывают картинку сохранения):
Line # Mem usage Increment Line Contents
================================================
8 53.1 MiB 53.1 MiB @profile
9 def main():
10 53.1 MiB 0.0 MiB with open("genome_matrix_header.txt") as header:
11 53.1 MiB 0.0 MiB header = header.read().rstrip('\n').split('\t')
12
13 2070.9 MiB 2017.8 MiB gen_matrix_df = pd.read_csv('genome_matrix_final-chr1234-1mb.txt', sep='\t', names=header)
14 2071.2 MiB 0.4 MiB gen_matrix_df = gen_matrix_df.drop(columns=[gen_matrix_df.keys()[0]])
15 2071.2 MiB 0.0 MiB gen_matrix_df = gen_matrix_df.drop(columns=[gen_matrix_df.keys()[0]])
16 2040.7 MiB -30.5 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
...
23 1827.1 MiB -30.5 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
24 1094.7 MiB -732.4 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
25 1765.9 MiB 671.3 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
26 1094.7 MiB -671.3 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
27 1704.8 MiB 610.2 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
28 1094.7 MiB -610.2 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
29 1643.9 MiB 549.2 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
30 1094.7 MiB -549.2 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
31 1582.8 MiB 488.1 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
32 1094.7 MiB -488.1 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
33 1521.9 MiB 427.2 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
34 1094.7 MiB -427.2 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
35 1460.8 MiB 366.1 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
36 1094.7 MiB -366.1 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
37 1094.7 MiB 0.0 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
...
47 1094.7 MiB 0.0 MiB gen_matrix_df = gen_matrix_df.drop(columns=[random.choice(gen_matrix_df.keys())])
Я хочу закончить этот раздел цитатой из свежей статьи о проблемах дизайна и будущих Pandas2 от первоначального автора Pandas.
Практическое правило панд: объем оперативной памяти в 5-10 раз больше размера вашего набора данных
Дерево процессов
Наконец, давайте подойдем к пулу и посмотрим, сможет ли использовать copy -o n-write. Мы будем использовать smemstat
(доступный из репозитория Ubuntu), чтобы оценить совместное использование памяти группой процессов и glances
для записи свободной памяти всей системы. Оба могут написать JSON.
Запустим оригинальный скрипт с Pool(2)
. Нам понадобятся 3 оконных окна.
-
smemstat -l -M -p "python3.6 script.py" -o smemstat.json 1
-
glances -t 1 --export-json glances.json
-
mprof run -M script.py
Тогда mprof plot
производит:
Диаграмма сумм (mprof run --nopython --include-children./script.py
) выглядит следующим образом:
Обратите внимание, что две диаграммы выше показывают RSS. Гипотеза состоит в том, что из-за копирования -o n-write это не отражает фактическое использование памяти. Теперь у нас есть два JSON файлов из smemstat
и glances
. Я приведу следующий скрипт для преобразования файлов JSON в CSV.
#!/usr/bin/env python3
import csv
import sys
import json
def smemstat():
with open('smemstat.json') as f:
smem = json.load(f)
rows = []
fieldnames = set()
for s in smem['smemstat']['periodic-samples']:
row = {}
for ps in s['smem-per-process']:
if 'script.py' in ps['command']:
for k in ('uss', 'pss', 'rss'):
row['{}-{}'.format(ps['pid'], k)] = ps[k] // 2 ** 20
# smemstat produces empty samples, backfill from previous
if rows:
for k, v in rows[-1].items():
row.setdefault(k, v)
rows.append(row)
fieldnames.update(row.keys())
with open('smemstat.csv', 'w') as out:
dw = csv.DictWriter(out, fieldnames=sorted(fieldnames))
dw.writeheader()
list(map(dw.writerow, rows))
def glances():
rows = []
fieldnames = ['available', 'used', 'cached', 'mem_careful', 'percent',
'free', 'mem_critical', 'inactive', 'shared', 'history_size',
'mem_warning', 'total', 'active', 'buffers']
with open('glances.csv', 'w') as out:
dw = csv.DictWriter(out, fieldnames=fieldnames)
dw.writeheader()
with open('glances.json') as f:
for l in f:
d = json.loads(l)
dw.writerow(d['mem'])
if __name__ == '__main__':
globals()[sys.argv[1]]()
Сначала давайте посмотрим на free
память.
Разница между первым и минимальным составляет ~ 4,15 ГиБ. А вот как выглядят цифры PSS:
И сумма:
Таким образом, мы видим, что из-за копирования -o n-запись фактическое потребление памяти составляет ~ 4,15 ГБ. Но мы все еще сериализуем данные для отправки их рабочим процессам через Pool.map
. Можем ли мы использовать здесь копию -o n-write?
Общие данные
Чтобы использовать copy -o n-write, нам нужно, чтобы list(gen_matrix_df_list.values())
был доступен глобально, чтобы рабочий после fork все еще мог его прочитать.
-
Позвольте изменить код после
del gen_matrix_df
вmain
следующим образом:... global global_gen_matrix_df_values global_gen_matrix_df_values = list(gen_matrix_df_list.values()) del gen_matrix_df_list p = Pool(2) result = p.map(matrix_to_vcf, range(len(global_gen_matrix_df_values))) ...
- Удалить
del gen_matrix_df_list
который идет позже. -
И измените первые строки
matrix_to_vcf
следующим образом:def matrix_to_vcf(i): matrix_df = global_gen_matrix_df_values[i]
Теперь давайте перезапустим его. Свободная память:
Дерево процессов:
И его сумма:
Таким образом, мы не превышаем ~ 2,9 ГБ фактического использования памяти (пик основного процесса имеет место при построении фрейма данных), и копирование -o n-write помогло!
Как примечание, есть так называемое copy -o n-read, поведение сборщика мусора ссылочного цикла Python, описанное в Instagram Engineering (которое привело к gc.freeze
в выпуске 315858). Но gc.disable()
не оказывает влияния в данном конкретном случае.
Обновить
Альтернативой копированию -o n-write copy -l ess данных может быть делегирование его ядру с самого начала с помощью numpy.memmap
. Вот пример реализации из High Performance Data Processing в Python. Хитрость затем сделать Панды использовать mmaped массив Numpy.