Какова стратегия python для управления распределением / освобождением больших переменных? - PullRequest
0 голосов
/ 04 марта 2019

В качестве продолжения этого вопроса представляется, что в (C) Python существуют разные стратегии распределения / освобождения для маленьких и больших переменных.
Точнее, кажется,граница размера объекта, выше которой память, используемая выделенным объектом, может быть возвращена ОС.Ниже этого размера память не возвращается ОС.

Чтобы процитировать ответ, взятый из политики Numpy для освобождения памяти:

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

Действительно, эти две стратегии распределения легко показать.Например:

  • 1-я стратегия: память не возвращается ОС
import numpy as np
import psutil
import gc

# Allocate  array
x = np.random.uniform(0,1, size=(10**4))

# gc
del x
gc.collect()
# We go from 41295.872 KB to 41295.872 KB
# using psutil.Process().memory_info().rss / 10**3; same behavior for VMS

=> Нет памяти, возвращаемой ОС

  • 2-я стратегия: освобожденная память возвращается ОС

При выполнении того же эксперимента, но с большим массивом:

x = np.random.uniform(0,1, size=(10**5))

del x
gc.collect()
# We go from 41582.592 KB to 41017.344 KB

=> Память освобождается дляОС

Кажется, что объекты, размер которых превышает 8*10**4 байтов, выделяются с использованием 2-й стратегии.

Итак:

  • Задокументировано ли это поведение?(И какова точная граница, на которой меняется стратегия распределения?)
  • Каковы внутренние элементы этих стратегий (больше, чем допущение использования mmap / munmap для освобождения памяти обратно вOS)
  • Это 100% сделано во время выполнения Python или у Numpy есть особый способ справиться с этим?( numpy doc упоминает NPY_USE_PYMEM, который переключается между распределителем памяти)

1 Ответ

0 голосов
/ 05 марта 2019

То, что вы наблюдаете, - это не стратегия CPython, а стратегия распределителя памяти, которая поставляется вместе со средой выполнения C, используемой вашей CPython-версией.

Когда CPython выделяет / освобождает память через malloc/free,он не взаимодействует напрямую с базовой ОС, а с конкретной реализацией распределителя памяти.В моем случае в Linux это Распределитель GNU .

Распределитель GNU имеет разные так называемые арены, где память не возвращается в ОС, но сохраняется, чтобы ее можно было использовать повторно.без необходимости общаться с ОС.Однако, если запрашивается большой объем памяти (независимо от определения «большой»), распределитель не использует память из арен, а запрашивает память из ОС и, как следствие, может вернуть ее непосредственно ОС, один раз free называется.


CPython имеет свой собственный распределитель памяти - pymalloc , который построен поверх C-runtime-allocator.Оптимизирован для небольших объектов, которые живут на специальной арене;при создании / освобождении этих объектов меньше затрат по сравнению с базовым C-runtime-allocator.Однако объекты размером более 512 байт не используют эту арену, а управляются непосредственно C-runtime-allocator.

Ситуация еще более сложна с массивом numpy, поскольку дляметаданные (например, форма, тип данных и другие флаги) и для самих фактических данных:

  1. Для метаданных PyArray_malloc, распределитель памяти CPython (т.е.pymalloc).
  2. Для самих данных используется PyDataMem_NEW, который напрямую использует базовую функциональность C-runtimme:
NPY_NO_EXPORT void *
PyDataMem_NEW(size_t size)
{
    void *result;

    result = malloc(size);
    ...
    return result;
}

Я не уверен, какова была точная идея этого дизайна: очевидно, что хотелось бы воспользоваться преимуществами оптимизации небольших объектов pymalloc, и для данных эта оптимизация никогда не будет работать, но тогда можно было бы использовать PyMem_RawMalloc вместо malloc.Возможно, цель состояла в том, чтобы иметь возможность обернуть массивы вокруг памяти, выделенной C-подпрограммами, и взять на себя владение памятью (но в некоторых случаях это не сработает, см. Мой комментарий в конце этого поста).

Это объясняет поведение, которое вы наблюдаете: Для данных (размер которых изменяется в зависимости от переданного аргумента size в) используется PyDataMem_NEW, который обходит распределитель памяти CPython, и вы видите оригинальное поведение распределителей C-runtime.


Следует избегать смешивания различных процедур выделения / освобождения PyArray_malloc / PyDataMem_NEW'/ malloc and PyArray_free / PyDataMem_FREE / free`: даже если это работает на OS + Pythonимеющаяся версия может не работать для других комбинаций.

Например, в Windows, когда расширение построено с другой версией компилятора, один исполняемый файл может иметь разные распределители памяти из разных C-run-time и malloc/free может связываться с различными C-памятью-распределителями, что может привести к затруднению отслеживания Доуn ошибок.

...